[ty] narrow the right-hand side of ==, !=, is and is not conditions when the left-hand side is not narrowable (#22511)

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This commit is contained in:
drbh
2026-01-13 11:01:54 -05:00
committed by GitHub
parent c7b41060f4
commit d13b5db066
4 changed files with 98 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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));
}
}
_ => {}
}
}