Add else-branch narrowing for if type(a) is A when A is @final (#19925)

This commit is contained in:
Alex Waygood 2025-08-15 14:52:30 +01:00 committed by GitHub
parent bd4506aac5
commit 6de84ed56e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 48 additions and 5 deletions

View file

@ -3,10 +3,15 @@
## `type(x) is C` ## `type(x) is C`
```py ```py
from typing import final
class A: ... class A: ...
class B: ... class B: ...
def _(x: A | B): @final
class C: ...
def _(x: A | B, y: A | C):
if type(x) is A: if type(x) is A:
reveal_type(x) # revealed: A reveal_type(x) # revealed: A
else: else:
@ -14,20 +19,55 @@ def _(x: A | B):
# of `x` could be a subclass of `A`, so we need # of `x` could be a subclass of `A`, so we need
# to infer the full union type: # to infer the full union type:
reveal_type(x) # revealed: A | B reveal_type(x) # revealed: A | B
if type(y) is C:
reveal_type(y) # revealed: C
else:
# here, however, inferring `A` is fine,
# because `C` is `@final`: no subclass of `A`
# and `C` could exist
reveal_type(y) # revealed: A
if type(y) is A:
reveal_type(y) # revealed: A
else:
# but here, `type(y)` could be a subclass of `A`,
# in which case the `type(y) is A` call would evaluate
# to `False` even if `y` was an instance of `A`,
# so narrowing cannot occur
reveal_type(y) # revealed: A | C
``` ```
## `type(x) is not C` ## `type(x) is not C`
```py ```py
from typing import final
class A: ... class A: ...
class B: ... class B: ...
def _(x: A | B): @final
class C: ...
def _(x: A | B, y: A | C):
if type(x) is not A: if type(x) is not A:
# Same reasoning as above: no narrowing should occur here. # Same reasoning as above: no narrowing should occur here.
reveal_type(x) # revealed: A | B reveal_type(x) # revealed: A | B
else: else:
reveal_type(x) # revealed: A reveal_type(x) # revealed: A
if type(y) is not C:
# same reasoning as above: narrowing *can* occur here because `C` is `@final`
reveal_type(y) # revealed: A
else:
reveal_type(y) # revealed: C
if type(y) is not A:
# same reasoning as above: narrowing *cannot* occur here
# because `A` is not `@final`
reveal_type(y) # revealed: A | C
else:
reveal_type(y) # revealed: A
``` ```
## `type(x) == C`, `type(x) != C` ## `type(x) == C`, `type(x) != C`

View file

@ -772,13 +772,15 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
_ => continue, _ => continue,
}; };
let is_valid_constraint = if is_positive { let is_positive = if is_positive {
op == &ast::CmpOp::Is op == &ast::CmpOp::Is
} else { } else {
op == &ast::CmpOp::IsNot op == &ast::CmpOp::IsNot
}; };
if !is_valid_constraint { // `else`-branch narrowing for `if type(x) is Y` can only be done
// if `Y` is a final class
if !rhs_class.is_final(self.db) && !is_positive {
continue; continue;
} }
@ -791,7 +793,8 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
let place = self.expect_place(&target); let place = self.expect_place(&target);
constraints.insert( constraints.insert(
place, place,
Type::instance(self.db, rhs_class.unknown_specialization(self.db)), Type::instance(self.db, rhs_class.unknown_specialization(self.db))
.negate_if(self.db, !is_positive),
); );
} }
} }