diff --git a/mypy/checker.py b/mypy/checker.py index 9c10cd2fc30d..8ff921996121 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -115,6 +115,7 @@ RaiseStmt, RefExpr, ReturnStmt, + SetExpr, StarExpr, Statement, StrExpr, @@ -5928,20 +5929,37 @@ def has_no_custom_eq_checks(t: Type) -> bool: ): if_map[operands[left_index]] = remove_optional(item_type) + right_iterable_expr = operands[right_index] if right_index in narrowable_operand_index_to_hash: if_type, else_type = self.conditional_types_for_iterable( item_type, iterable_type ) - expr = operands[right_index] if if_type is None: if_map = None else: - if_map[expr] = if_type + if_map[right_iterable_expr] = if_type if else_type is None: else_map = None else: - else_map[expr] = else_type + else_map[right_iterable_expr] = else_type + # check for `None in ` + if ( + isinstance(get_proper_type(item_type), NoneType) + and isinstance(right_iterable_expr, (ListExpr, TupleExpr, SetExpr)) + # Ensure the iterable does not inherently contain None literals + and not any( + is_literal_none(iterable_item) + for iterable_item in right_iterable_expr.items + ) + ): + if else_map is None: + else_map = {} + for iterable_item in right_iterable_expr.items: + proper_item_type = self.lookup_type(iterable_item) + # Remove the option of the current item to be `None` for the entire else scope + if is_overlapping_none(proper_item_type): + else_map[iterable_item] = remove_optional(proper_item_type) else: if_map = {} else_map = {} @@ -6006,8 +6024,8 @@ def has_no_custom_eq_checks(t: Type) -> bool: and_conditional_maps(left_else_vars, right_else_vars), ) elif isinstance(node, UnaryExpr) and node.op == "not": - left, right = self.find_isinstance_check(node.expr) - return right, left + left, iterable_expr = self.find_isinstance_check(node.expr) + return iterable_expr, left elif ( literal(node) == LITERAL_TYPE and self.has_type(node) diff --git a/test-data/unit/check-optional.test b/test-data/unit/check-optional.test index 70f3c4486e14..98d7f93092cf 100644 --- a/test-data/unit/check-optional.test +++ b/test-data/unit/check-optional.test @@ -93,6 +93,71 @@ else: reveal_type(x) # N: Revealed type is "Any" [builtins fixtures/bool.pyi] +[case testRefinementAfterNoneInCheck] +from typing import Optional, List + +def refine_types(a: Optional[int], b: Optional[str], c: Optional[float]) -> str: + if None in [a, b, c]: + return "One or more are None" + else: + reveal_type(a) # N: Revealed type is "builtins.int" + reveal_type(b) # N: Revealed type is "builtins.str" + reveal_type(c) # N: Revealed type is "builtins.float" + return f"{a}, {b}, {c}" + +a: Optional[int] = 5 +b: Optional[str] = "hello" +c: Optional[float] = None + +result = refine_types(a, b, c) +reveal_type(result) # N: Revealed type is "builtins.str" + +[case testRefinementWithComplexCondition] +from typing import Optional, List + +def refine_complex(a: Optional[int], b: Optional[float], c: Optional[str]) -> str: + # Test with complex conditions mixed with `None in` + if None not in (a, b, c) and b + 3 != 5.5: + reveal_type(a) # N: Revealed type is "builtins.int" + reveal_type(b) # N: Revealed type is "builtins.float" + reveal_type(c) # N: Revealed type is "builtins.str" + return f"{a}, {b}, {c}" + reveal_type(a) # N: Revealed type is "Union[builtins.int, None]" + reveal_type(b) # N: Revealed type is "Union[builtins.float, None]" + reveal_type(c) # N: Revealed type is "Union[builtins.str, None]" + return "All are valid" + +a: Optional[int] = 5 +b: Optional[float] = 10.3 +c: Optional[str] = "cCCCc" + +result_complex = refine_complex(a, b, c) +reveal_type(result_complex) # N: Revealed type is "builtins.str" + +[case testRefinementFailureWhenNonePresent] +from typing import Optional, List + +def check_failure(a: Optional[int], b: Optional[float], c: Optional[str]) -> str: + if None in {a, b, c}: + print(a + 3) # E: Unsupported operand types for + ("None" and "int") [operator] + print(b.is_integer()) # E: Item "None" of "float | None" has no attribute "is_integer" [union-attr] + print(c.upper()) # E: Item "None" of "str | None" has no attribute "upper" [union-attr] + return "None is present" + else: + print(a + 3) + print(b.is_integer()) + print(c.upper()) + return "All are valid" + +[case testNotRefiningWhenNoneIsInTheIterable] +def not_refine_none_in_iterable(a: Optional[int], b: Optional[float], c: Optional[str]) -> str: + # Test with complex conditions mixed with `None in` + if None not in {a, b, c, None} and b + 3 != 5.5: # E: Unsupported operand types for + ("None" and "int") + reveal_type(a) # N: Revealed type is "Union[builtins.int, None]" + reveal_type(b) # N: Revealed type is "Union[builtins.float, None]" + reveal_type(c) # N: Revealed type is "Union[builtins.str, None]" + return "Bla Bla" + [case testOrCases] from typing import Optional x = None # type: Optional[str]