From aeb094effead60c358c1860e2099c5d2fb3f5dd4 Mon Sep 17 00:00:00 2001 From: paidax <2338239869@qq.com> Date: Sun, 6 Sep 2026 18:13:45 +0800 Subject: [PATCH] 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 --- mypy/checker.py | 51 +++++++++++++++++- mypy/patterns.py | 16 ++++++ test-data/unit/check-python310.test | 82 +++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 33ed5387554d8..cf99be9d1f13c 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 a01bf6acc8766..5bd6a96dc3ee5 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 01e490d0da507..af51302ef272a 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