diff --git a/mypy/checkpattern.py b/mypy/checkpattern.py index 9d02262a1530..a27bd23395b1 100644 --- a/mypy/checkpattern.py +++ b/mypy/checkpattern.py @@ -358,21 +358,28 @@ def visit_sequence_pattern(self, o: SequencePattern) -> PatternType: new_type = UninhabitedType() else: new_type = TupleType(new_inner_types, current_type.partial_fallback) - - num_always_match = sum(is_uninhabited(typ) for typ in rest_inner_types) - if num_always_match == len(rest_inner_types): - # All subpatterns always match, so we can apply negative narrowing - rest_type = UninhabitedType() - elif num_always_match == len(rest_inner_types) - 1: - # Exactly one subpattern may conditionally match, the rest always match. - # We can apply negative narrowing to this one position. - rest_type = TupleType( - [ - curr if is_uninhabited(rest) else rest - for curr, rest in zip(inner_types, rest_inner_types) - ], - current_type.partial_fallback, - ) + # A value doesn't match the pattern if some item doesn't match its + # subpattern while all the items before it do, so what remains is the + # union of one tuple per position whose subpattern may fail to match: + # tuple[A, B] - tuple[a, b] == tuple[A - a, B] | tuple[a, B - b] + # Positions whose subpattern always matches never fail and keep their type. + matched_types = [ + curr if is_uninhabited(rest) else new + for curr, new, rest in zip(inner_types, new_inner_types, rest_inner_types) + ] + rest_items = [ + TupleType( + matched_types[:i] + [rest] + inner_types[i + 1 :], + current_type.partial_fallback, + ) + for i, rest in enumerate(rest_inner_types) + if not is_uninhabited(rest) + ] + if rest_items: + rest_type = make_simplified_union(rest_items) + else: + # All subpatterns always match, so nothing remains + rest_type = UninhabitedType() elif isinstance(current_type, TupleType): # For variadic tuples it is too tricky to match individual items like for fixed # tuples, so we instead try to narrow the entire type. diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 01e490d0da50..3b754efec022 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -2083,10 +2083,95 @@ match m6: case (1, "a"): reveal_type(m6) # N: Revealed type is "tuple[Literal[1], Literal['a']]" case _: - reveal_type(m6) # N: Revealed type is "tuple[Literal[1] | Literal[2], Literal['a'] | Literal['b']]" + reveal_type(m6) # N: Revealed type is "tuple[Literal[2], Literal['a'] | Literal['b']] | tuple[Literal[1], Literal['b']]" [builtins fixtures/tuple.pyi] +[case testMatchSequencePatternNegativeNarrowingMultiplePositions] +# flags: --strict-equality --warn-unreachable +from typing import Literal, Tuple + +m1: Tuple[bool, bool] + +match m1: + case (True, True): + reveal_type(m1) # N: Revealed type is "tuple[Literal[True], Literal[True]]" + case (True, False): + reveal_type(m1) # N: Revealed type is "tuple[Literal[True], Literal[False]]" + case (False, True): + reveal_type(m1) # N: Revealed type is "tuple[Literal[False], Literal[True]]" + case _: + reveal_type(m1) # N: Revealed type is "tuple[Literal[False], Literal[False]]" + +m2: Tuple[bool, int, bool] + +match m2: + case (True, _, True): + reveal_type(m2) # N: Revealed type is "tuple[Literal[True], builtins.int, Literal[True]]" + case _: + reveal_type(m2) # N: Revealed type is "tuple[Literal[False], builtins.int, builtins.bool] | tuple[Literal[True], builtins.int, Literal[False]]" + +m3: Tuple[Literal[1], Literal[2]] + +match m3: + case (1, 3): + reveal_type(m3) # E: Statement is unreachable + case _: + reveal_type(m3) # N: Revealed type is "tuple[Literal[1], Literal[2]]" + +def f(a: bool, b: bool) -> None: + match a, b: + case True, True: + reveal_type(a) # N: Revealed type is "Literal[True]" + reveal_type(b) # N: Revealed type is "Literal[True]" + case True, False: + reveal_type(a) # N: Revealed type is "Literal[True]" + reveal_type(b) # N: Revealed type is "Literal[False]" + case False, _: + reveal_type(a) # N: Revealed type is "Literal[False]" + reveal_type(b) # N: Revealed type is "builtins.bool" + case _: + reveal_type(a) # E: Statement is unreachable +[builtins fixtures/tuple.pyi] + +[case testMatchExhaustiveTupleOfBoolsAndEnums] +# flags: --enable-error-code exhaustive-match +from enum import Enum +from typing import Tuple + +class Color(Enum): + RED = 1 + BLUE = 2 + +def f1(a: bool, b: bool) -> float: + match a, b: + case True, True: + return 1 + case True, False: + return 0.9 + case False, True: + return 0.5 + case False, False: + return 0 + +def f2(t: Tuple[Color, Color]) -> int: + match t: + case (Color.RED, Color.RED): + return 1 + case (Color.RED, Color.BLUE): + return 2 + case (Color.BLUE, _): + return 3 + +def f3(a: bool, b: bool) -> int: # E: Missing return statement + match a, b: # E: Match statement has unhandled case for values of type "tuple[Literal[False], Literal[False]]" \ + # N: If match statement is intended to be non-exhaustive, add `case _: pass` + case True, _: + return 1 + case False, True: + return 2 +[builtins fixtures/tuple.pyi] + [case testMatchSequenceWildcardRefutability] # flags: --strict-equality --warn-unreachable def f1(x: int | list[int]):