Skip to content

Commit 07f8112

Browse files
committed
Report error for irrefutable patterns that make remaining match patterns unreachable
CPython rejects a match statement at compile time when an unguarded irrefutable pattern (a capture or wildcard) appears in any case except the last one, and when an irrefutable alternative appears in a non-final position of an or pattern. mypy accepted both without any error, so a file that cannot even be imported checked clean. Add the check to semantic analysis, where every pattern is visited unconditionally, so it also fires in unchecked functions the same way the runtime SyntaxError does. Fixes #21925
1 parent 3ad6157 commit 07f8112

4 files changed

Lines changed: 163 additions & 0 deletions

File tree

mypy/message_registry.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,12 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
281281
# Match Statement
282282
MISSING_MATCH_ARGS: Final = 'Class "{}" doesn\'t define "__match_args__"'
283283
OR_PATTERN_ALTERNATIVE_NAMES: Final = "Alternative patterns bind different names"
284+
NAME_CAPTURE_MAKES_REMAINING_UNREACHABLE: Final = (
285+
'Name capture "{}" makes remaining patterns unreachable'
286+
)
287+
WILDCARD_MAKES_REMAINING_UNREACHABLE: Final = (
288+
"Wildcard pattern makes remaining patterns unreachable"
289+
)
284290
CLASS_PATTERN_GENERIC_TYPE_ALIAS: Final = (
285291
"Class pattern class must not be a type alias with type parameters"
286292
)

mypy/patterns.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,19 @@ def __init__(
148148

149149
def accept(self, visitor: PatternVisitor[T]) -> T:
150150
return visitor.visit_class_pattern(self)
151+
152+
153+
def get_irrefutable_pattern(pattern: Pattern) -> AsPattern | None:
154+
"""Return the capture or wildcard pattern that makes this pattern irrefutable.
155+
156+
An irrefutable pattern matches any subject: a capture pattern, a wildcard
157+
pattern, an as pattern whose subpattern is irrefutable, or an or pattern
158+
whose last alternative is irrefutable. Returns None for refutable patterns.
159+
"""
160+
if isinstance(pattern, AsPattern):
161+
if pattern.pattern is None:
162+
return pattern
163+
return get_irrefutable_pattern(pattern.pattern)
164+
if isinstance(pattern, OrPattern):
165+
return get_irrefutable_pattern(pattern.patterns[-1])
166+
return None

mypy/semanal.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@
207207
SingletonPattern,
208208
StarredPattern,
209209
ValuePattern,
210+
get_irrefutable_pattern,
210211
)
211212
from mypy.plugin import (
212213
ClassDefContext,
@@ -5785,12 +5786,28 @@ def visit_match_stmt(self, s: MatchStmt) -> None:
57855786
infer_reachability_of_match_statement(s, self.options)
57865787
s.subject.accept(self)
57875788
for i in range(len(s.patterns)):
5789+
# An unguarded irrefutable pattern is only allowed in the last case,
5790+
# otherwise the remaining cases could never match. CPython rejects
5791+
# such match statements at compile time with a SyntaxError.
5792+
if i < len(s.patterns) - 1 and s.guards[i] is None:
5793+
irrefutable = get_irrefutable_pattern(s.patterns[i])
5794+
if irrefutable is not None:
5795+
self.fail_irrefutable_pattern(irrefutable)
57885796
s.patterns[i].accept(self)
57895797
guard = s.guards[i]
57905798
if guard is not None:
57915799
guard.accept(self)
57925800
self.visit_block(s.bodies[i])
57935801

5802+
def fail_irrefutable_pattern(self, pattern: AsPattern) -> None:
5803+
if pattern.name is None:
5804+
msg = message_registry.WILDCARD_MAKES_REMAINING_UNREACHABLE
5805+
else:
5806+
msg = message_registry.NAME_CAPTURE_MAKES_REMAINING_UNREACHABLE.format(
5807+
pattern.name.name
5808+
)
5809+
self.fail(msg, pattern, serious=True)
5810+
57945811
def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None:
57955812
if s.invalid_recursive_alias:
57965813
return
@@ -6555,6 +6572,13 @@ def visit_as_pattern(self, p: AsPattern) -> None:
65556572
self.analyze_lvalue(p.name)
65566573

65576574
def visit_or_pattern(self, p: OrPattern) -> None:
6575+
# An irrefutable alternative is only allowed in the last position,
6576+
# regardless of where the or pattern appears. CPython rejects other
6577+
# placements at compile time with a SyntaxError.
6578+
for pattern in p.patterns[:-1]:
6579+
irrefutable = get_irrefutable_pattern(pattern)
6580+
if irrefutable is not None:
6581+
self.fail_irrefutable_pattern(irrefutable)
65586582
for pattern in p.patterns:
65596583
pattern.accept(self)
65606584

test-data/unit/check-python310.test

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4005,3 +4005,120 @@ def enum_then_dummy_class(arg: DummyClass | Literal[MyEnum.RELEVANT]):
40054005
case _:
40064006
pass # E: Statement is unreachable
40074007
[builtins fixtures/tuple.pyi]
4008+
4009+
4010+
[case testMatchIrrefutablePatternNotLastCase]
4011+
# Irrefutable patterns before the last case are a SyntaxError at runtime
4012+
def capture(x: int) -> None:
4013+
match x:
4014+
case y: # E: Name capture "y" makes remaining patterns unreachable
4015+
pass
4016+
case 1:
4017+
pass
4018+
4019+
def wildcard(x: int) -> None:
4020+
match x:
4021+
case _: # E: Wildcard pattern makes remaining patterns unreachable
4022+
pass
4023+
case 1:
4024+
pass
4025+
4026+
def as_capture(x: int) -> None:
4027+
match x:
4028+
case (y as z): # E: Name capture "y" makes remaining patterns unreachable
4029+
pass
4030+
case 1:
4031+
pass
4032+
4033+
def or_ending_irrefutable(x: int) -> None:
4034+
match x:
4035+
case 1 | _: # E: Wildcard pattern makes remaining patterns unreachable
4036+
pass
4037+
case 2:
4038+
pass
4039+
[builtins fixtures/tuple.pyi]
4040+
4041+
[case testMatchIrrefutablePatternAllowed]
4042+
def capture_last(x: int) -> None:
4043+
match x:
4044+
case 1:
4045+
pass
4046+
case y:
4047+
pass
4048+
4049+
def wildcard_last(x: int) -> None:
4050+
match x:
4051+
case 1:
4052+
pass
4053+
case _:
4054+
pass
4055+
4056+
def check(v: int) -> bool: ...
4057+
4058+
def guarded_capture(x: int) -> None:
4059+
match x:
4060+
case y if check(y):
4061+
pass
4062+
case _:
4063+
pass
4064+
4065+
def guarded_wildcard(x: int) -> None:
4066+
match x:
4067+
case _ if check(x):
4068+
pass
4069+
case 1:
4070+
pass
4071+
4072+
def matches_anything_but_refutable(x: int) -> None:
4073+
match x:
4074+
case int():
4075+
pass
4076+
case _:
4077+
pass
4078+
[builtins fixtures/tuple.pyi]
4079+
4080+
[case testMatchIrrefutableOrPatternAlternativeNotLast]
4081+
# An irrefutable or pattern alternative is only allowed in the last position,
4082+
# even in the last case, in a guarded case, or nested in another pattern
4083+
def wildcard_alternative_last_case(x: int) -> None:
4084+
match x:
4085+
case 1:
4086+
pass
4087+
case _ | 2: # E: Wildcard pattern makes remaining patterns unreachable
4088+
pass
4089+
4090+
def check(v: int) -> bool: ...
4091+
4092+
def wildcard_alternative_guarded(x: int) -> None:
4093+
match x:
4094+
case _ | 1 if check(x): # E: Wildcard pattern makes remaining patterns unreachable
4095+
pass
4096+
case 2:
4097+
pass
4098+
4099+
def nested_in_sequence(x: object) -> None:
4100+
match x:
4101+
case [_ | 1, y]: # E: Wildcard pattern makes remaining patterns unreachable
4102+
pass
4103+
4104+
def nested_in_or(x: int) -> None:
4105+
match x:
4106+
case 1 | (2 | _) | 3: # E: Wildcard pattern makes remaining patterns unreachable
4107+
pass
4108+
4109+
def or_ending_irrefutable_last_case(x: int) -> None:
4110+
match x:
4111+
case 1:
4112+
pass
4113+
case 2 | _:
4114+
pass
4115+
[builtins fixtures/tuple.pyi]
4116+
4117+
[case testMatchIrrefutablePatternUncheckedFunction]
4118+
def f(x):
4119+
match x:
4120+
case y: # E: Name capture "y" makes remaining patterns unreachable
4121+
pass
4122+
case 1:
4123+
pass
4124+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)