diff --git a/mypy/checker.py b/mypy/checker.py index b6a9bb3b22cd..2dd7f10e6f35 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -7905,6 +7905,10 @@ def is_writable_attribute(self, node: Node) -> bool: return False def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None: + """Get the type(s) resulting from an isinstance check. + + Returns an empty list for isinstance(x, ()). + """ if isinstance(expr, OpExpr) and expr.op == "|": left = self.get_isinstance_type(expr.left) if left is None and is_literal_none(expr.left): @@ -7944,11 +7948,6 @@ def get_isinstance_type(self, expr: Expression) -> list[TypeRange] | None: types.append(TypeRange(typ, is_upper_bound=False)) else: # we didn't see an actual type, but rather a variable with unknown value return None - if not types: - # this can happen if someone has empty tuple as 2nd argument to isinstance - # strictly speaking, we should return UninhabitedType but for simplicity we will simply - # refuse to do any type inference for now - return None return types def is_literal_enum(self, n: Expression) -> bool: @@ -8185,59 +8184,82 @@ def conditional_types( UninhabitedType means unreachable. None means no new information can be inferred. """ - if proposed_type_ranges: - if len(proposed_type_ranges) == 1: - target = proposed_type_ranges[0].item - target = get_proper_type(target) - if isinstance(target, LiteralType) and ( - target.is_enum_literal() or isinstance(target.value, bool) - ): - enum_name = target.fallback.type.fullname - current_type = try_expanding_sum_type_to_union(current_type, enum_name) - proposed_items = [type_range.item for type_range in proposed_type_ranges] - proposed_type = make_simplified_union(proposed_items) - if isinstance(get_proper_type(current_type), AnyType): - return proposed_type, current_type - elif isinstance(proposed_type, AnyType): - # We don't really know much about the proposed type, so we shouldn't - # attempt to narrow anything. Instead, we broaden the expr to Any to - # avoid false positives - return proposed_type, default - elif not any(type_range.is_upper_bound for type_range in proposed_type_ranges) and ( - # concrete subtypes - is_proper_subtype(current_type, proposed_type, ignore_promotions=True) - # structural subtypes - or ( - ( - isinstance(proposed_type, CallableType) - or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol) - ) - and is_subtype(current_type, proposed_type, ignore_promotions=True) - ) + if proposed_type_ranges is None: + # An isinstance check, but we don't understand the type + return current_type, default + + if not proposed_type_ranges: + # This is the case for `if isinstance(x, ())` which always returns False. + return UninhabitedType(), default + + if len(proposed_type_ranges) == 1: + # expand e.g. bool -> Literal[True] | Literal[False] + target = proposed_type_ranges[0].item + target = get_proper_type(target) + if isinstance(target, LiteralType) and ( + target.is_enum_literal() or isinstance(target.value, bool) ): - # Expression is always of one of the types in proposed_type_ranges - return default, UninhabitedType() - elif not is_overlapping_types(current_type, proposed_type, ignore_promotions=True): - # Expression is never of any type in proposed_type_ranges - return UninhabitedType(), default - else: - # we can only restrict when the type is precise, not bounded - proposed_precise_type = UnionType.make_union( - [ - type_range.item - for type_range in proposed_type_ranges - if not type_range.is_upper_bound - ] - ) - remaining_type = restrict_subtype_away( - current_type, - proposed_precise_type, + enum_name = target.fallback.type.fullname + current_type = try_expanding_sum_type_to_union(current_type, enum_name) + + proper_type = get_proper_type(current_type) + # factorize over union types: isinstance(A|B, C) -> yes = A_yes | B_yes + if isinstance(proper_type, UnionType): + result: list[tuple[Type | None, Type | None]] = [ + conditional_types( + union_item, + proposed_type_ranges, + default=union_item, consider_runtime_isinstance=consider_runtime_isinstance, ) - return proposed_type, remaining_type + for union_item in get_proper_types(proper_type.items) + ] + # separate list of tuples into two lists + yes_types, no_types = zip(*result) + proposed_type = make_simplified_union([t for t in yes_types if t is not None]) else: - # An isinstance check, but we don't understand the type - return current_type, default + proposed_items = [type_range.item for type_range in proposed_type_ranges] + proposed_type = make_simplified_union(proposed_items) + + if isinstance(proper_type, AnyType): + return proposed_type, current_type + elif isinstance(proposed_type, AnyType): + # We don't really know much about the proposed type, so we shouldn't + # attempt to narrow anything. Instead, we broaden the expr to Any to + # avoid false positives + return proposed_type, default + elif not any(type_range.is_upper_bound for type_range in proposed_type_ranges) and ( + # concrete subtypes + is_proper_subtype(current_type, proposed_type, ignore_promotions=True) + # structural subtypes + or ( + ( + isinstance(proposed_type, CallableType) + or (isinstance(proposed_type, Instance) and proposed_type.type.is_protocol) + ) + and is_subtype(current_type, proposed_type, ignore_promotions=True) + ) + ): + # Expression is always of one of the types in proposed_type_ranges + return default, UninhabitedType() + elif not is_overlapping_types(current_type, proposed_type, ignore_promotions=True): + # Expression is never of any type in proposed_type_ranges + return UninhabitedType(), default + else: + # we can only restrict when the type is precise, not bounded + proposed_precise_type = UnionType.make_union( + [ + type_range.item + for type_range in proposed_type_ranges + if not type_range.is_upper_bound + ] + ) + remaining_type = restrict_subtype_away( + current_type, + proposed_precise_type, + consider_runtime_isinstance=consider_runtime_isinstance, + ) + return proposed_type, remaining_type def conditional_types_to_typemaps( diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 5043d5422108..acd4b588f98c 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1483,11 +1483,12 @@ def f(x: Union[int, A], a: Type[A]) -> None: [builtins fixtures/isinstancelist.pyi] [case testIsInstanceWithEmtpy2ndArg] +# flags: --warn-unreachable from typing import Union def f(x: Union[int, str]) -> None: if isinstance(x, ()): - reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" + reveal_type(x) # E: Statement is unreachable else: reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]" [builtins fixtures/isinstancelist.pyi] diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index 7fffd3ce94e5..00d33c86414f 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -2639,7 +2639,7 @@ def baz(item: Base) -> None: reveal_type(item) # N: Revealed type is "Union[__main__., __main__.]" if isinstance(item, FooMixin): - reveal_type(item) # N: Revealed type is "__main__.FooMixin" + reveal_type(item) # N: Revealed type is "__main__." item.foo() else: reveal_type(item) # N: Revealed type is "__main__." diff --git a/test-data/unit/check-python310.test b/test-data/unit/check-python310.test index 3a9f12e8a550..1e27e30d4b04 100644 --- a/test-data/unit/check-python310.test +++ b/test-data/unit/check-python310.test @@ -1936,6 +1936,22 @@ def union(x: str | bool) -> None: reveal_type(x) # N: Revealed type is "Union[builtins.str, Literal[False]]" [builtins fixtures/tuple.pyi] +[case testMatchNarrowDownUnionUsingClassPattern] + +class Foo: ... +class Bar(Foo): ... + +def test_1(bar: Bar) -> None: + match bar: + case Foo() as foo: + reveal_type(foo) # N: Revealed type is "__main__.Bar" + +def test_2(bar: Bar | str) -> None: + match bar: + case Foo() as foo: + reveal_type(foo) # N: Revealed type is "__main__.Bar" + + [case testMatchAssertFalseToSilenceFalsePositives] class C: a: int | str