Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
16 changes: 16 additions & 0 deletions mypy/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
82 changes: 82 additions & 0 deletions test-data/unit/check-python310.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading