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
6 changes: 6 additions & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
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 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
24 changes: 24 additions & 0 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@
SingletonPattern,
StarredPattern,
ValuePattern,
get_irrefutable_pattern,
)
from mypy.plugin import (
ClassDefContext,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
117 changes: 117 additions & 0 deletions test-data/unit/check-python310.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Loading