diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md index f9de673a17..cb61ecf5f6 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/eq.md @@ -12,6 +12,30 @@ def _(flag: bool): reveal_type(x) # revealed: None ``` +## `None != x` (reversed operands) + +```py +def _(flag: bool): + x = None if flag else 1 + + if None != x: + reveal_type(x) # revealed: Literal[1] + else: + reveal_type(x) # revealed: None +``` + +This also works for `==` with reversed operands: + +```py +def _(flag: bool): + x = None if flag else 1 + + if None == x: + reveal_type(x) # revealed: None + else: + reveal_type(x) # revealed: Literal[1] +``` + ## `!=` for other singleton types ### Bool diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md index 79bb4c1d92..660ef375e7 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/in.md @@ -121,6 +121,31 @@ def test(x: Literal["a", "b", "c"] | None | int = None): reveal_type(x) # revealed: Literal["a", "c"] | int ``` +## No narrowing for the right-hand side (currently) + +No narrowing is done for the right-hand side currently, even if the right-hand side is a valid +"target" (name/attribute/subscript) that could potentially be narrowed. We may change this in the +future: + +```py +from typing import Literal + +def f(x: Literal["abc", "def"]): + if "a" in x: + # `x` could also be validly narrowed to `Literal["abc"]` here: + reveal_type(x) # revealed: Literal["abc", "def"] + else: + # `x` could also be validly narrowed to `Literal["def"]` here: + reveal_type(x) # revealed: Literal["abc", "def"] + + if "a" not in x: + # `x` could also be validly narrowed to `Literal["def"]` here: + reveal_type(x) # revealed: Literal["abc", "def"] + else: + # `x` could also be validly narrowed to `Literal["abc"]` here: + reveal_type(x) # revealed: Literal["abc", "def"] +``` + ## bool ```py diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md index 0c4e87c36c..c85e42fc6f 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/conditionals/is_not.md @@ -16,6 +16,32 @@ def _(flag: bool): reveal_type(x) # revealed: None | Literal[1] ``` +## `None is not x` (reversed operands) + +```py +def _(flag: bool): + x = None if flag else 1 + + if None is not x: + reveal_type(x) # revealed: Literal[1] + else: + reveal_type(x) # revealed: None + + reveal_type(x) # revealed: None | Literal[1] +``` + +This also works for other singleton types with reversed operands: + +```py +def _(flag: bool): + x = True if flag else False + + if False is not x: + reveal_type(x) # revealed: Literal[True] + else: + reveal_type(x) # revealed: Literal[False] +``` + ## `is not` for other singleton types Boolean literals: diff --git a/crates/ty_python_semantic/src/types/narrow.rs b/crates/ty_python_semantic/src/types/narrow.rs index 49461f9d06..435a1c6b64 100644 --- a/crates/ty_python_semantic/src/types/narrow.rs +++ b/crates/ty_python_semantic/src/types/narrow.rs @@ -1250,6 +1250,29 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> { ); } } + // For symmetric operators (==, !=, is, is not), if left is not a narrowable target, + // try to narrow the right operand instead by swapping the operands. + // E.g., `None != x` should narrow `x` the same way as `x != None`. + _ if matches!( + op, + ast::CmpOp::Eq | ast::CmpOp::NotEq | ast::CmpOp::Is | ast::CmpOp::IsNot + ) && matches!( + right, + ast::Expr::Name(_) + | ast::Expr::Attribute(_) + | ast::Expr::Subscript(_) + | ast::Expr::Named(_) + ) => + { + if let Some(right_place) = place_expr(right) + // Swap lhs_ty and rhs_ty since we're narrowing the right operand + && let Some(ty) = + self.evaluate_expr_compare_op(rhs_ty, lhs_ty, *op, is_positive) + { + let place = self.expect_place(&right_place); + constraints.insert(place, NarrowingConstraint::regular(ty)); + } + } _ => {} } }