mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 12:29:48 +00:00
[red-knot] Treat empty intersection as 'object', fix intersection simplification (#13880)
## Summary - Properly treat the empty intersection as being of type `object`. - Consequently, change the simplification method to explicitly add `Never` to the positive side of the intersection when collapsing a type such as `int & str` to `Never`, as opposed to just clearing both the positive and the negative side. - Minor code improvement in `bindings_ty`: use `peekable()` to check whether the iterator over constraints is empty, instead of handling first and subsequent elements separately. fixes #13870 ## Test Plan - New unit tests for `IntersectionBuilder` to make sure the empty intersection represents `object`. - Markdown-based regression test for the original issue in #13870
This commit is contained in:
parent
5d4edd61bf
commit
c6ce52c29e
3 changed files with 37 additions and 12 deletions
|
@ -1,5 +1,7 @@
|
||||||
# Narrowing for nested conditionals
|
# Narrowing for nested conditionals
|
||||||
|
|
||||||
|
## Multiple negative contributions
|
||||||
|
|
||||||
```py
|
```py
|
||||||
def int_instance() -> int: ...
|
def int_instance() -> int: ...
|
||||||
|
|
||||||
|
@ -11,3 +13,14 @@ if x != 1:
|
||||||
if x != 3:
|
if x != 3:
|
||||||
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
reveal_type(x) # revealed: int & ~Literal[1] & ~Literal[2] & ~Literal[3]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Multiple negative contributions with simplification
|
||||||
|
|
||||||
|
```py
|
||||||
|
x = 1 if flag1 else 2 if flag2 else 3
|
||||||
|
|
||||||
|
if x != 1:
|
||||||
|
reveal_type(x) # revealed: Literal[2, 3]
|
||||||
|
if x != 2:
|
||||||
|
reveal_type(x) # revealed: Literal[3]
|
||||||
|
```
|
||||||
|
|
|
@ -148,18 +148,18 @@ fn bindings_ty<'db>(
|
||||||
binding,
|
binding,
|
||||||
constraints,
|
constraints,
|
||||||
}| {
|
}| {
|
||||||
let mut constraint_tys =
|
let mut constraint_tys = constraints
|
||||||
constraints.filter_map(|constraint| narrowing_constraint(db, constraint, binding));
|
.filter_map(|constraint| narrowing_constraint(db, constraint, binding))
|
||||||
|
.peekable();
|
||||||
|
|
||||||
let binding_ty = binding_ty(db, binding);
|
let binding_ty = binding_ty(db, binding);
|
||||||
if let Some(first_constraint_ty) = constraint_tys.next() {
|
if constraint_tys.peek().is_some() {
|
||||||
let mut builder = IntersectionBuilder::new(db);
|
constraint_tys
|
||||||
builder = builder
|
.fold(
|
||||||
.add_positive(binding_ty)
|
IntersectionBuilder::new(db).add_positive(binding_ty),
|
||||||
.add_positive(first_constraint_ty);
|
IntersectionBuilder::add_positive,
|
||||||
for constraint_ty in constraint_tys {
|
)
|
||||||
builder = builder.add_positive(constraint_ty);
|
.build()
|
||||||
}
|
|
||||||
builder.build()
|
|
||||||
} else {
|
} else {
|
||||||
binding_ty
|
binding_ty
|
||||||
}
|
}
|
||||||
|
|
|
@ -253,6 +253,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||||
// A & B = Never if A and B are disjoint
|
// A & B = Never if A and B are disjoint
|
||||||
if new_positive.is_disjoint_from(db, *existing_positive) {
|
if new_positive.is_disjoint_from(db, *existing_positive) {
|
||||||
*self = Self::new();
|
*self = Self::new();
|
||||||
|
self.positive.insert(Type::Never);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -265,6 +266,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||||
// S & ~T = Never if S <: T
|
// S & ~T = Never if S <: T
|
||||||
if new_positive.is_subtype_of(db, *existing_negative) {
|
if new_positive.is_subtype_of(db, *existing_negative) {
|
||||||
*self = Self::new();
|
*self = Self::new();
|
||||||
|
self.positive.insert(Type::Never);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// A & ~B = A if A and B are disjoint
|
// A & ~B = A if A and B are disjoint
|
||||||
|
@ -328,6 +330,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||||
// S & ~T = Never if S <: T
|
// S & ~T = Never if S <: T
|
||||||
if existing_positive.is_subtype_of(db, new_negative) {
|
if existing_positive.is_subtype_of(db, new_negative) {
|
||||||
*self = Self::new();
|
*self = Self::new();
|
||||||
|
self.positive.insert(Type::Never);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// A & ~B = A if A and B are disjoint
|
// A & ~B = A if A and B are disjoint
|
||||||
|
@ -351,7 +354,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||||
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
|
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
|
||||||
self.simplify_unbound();
|
self.simplify_unbound();
|
||||||
match (self.positive.len(), self.negative.len()) {
|
match (self.positive.len(), self.negative.len()) {
|
||||||
(0, 0) => Type::Never,
|
(0, 0) => KnownClass::Object.to_instance(db),
|
||||||
(1, 0) => self.positive[0],
|
(1, 0) => self.positive[0],
|
||||||
_ => {
|
_ => {
|
||||||
self.positive.shrink_to_fit();
|
self.positive.shrink_to_fit();
|
||||||
|
@ -523,6 +526,15 @@ mod tests {
|
||||||
assert_eq!(intersection.neg_vec(&db), &[t0]);
|
assert_eq!(intersection.neg_vec(&db), &[t0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_intersection_empty_intersection_equals_object() {
|
||||||
|
let db = setup_db();
|
||||||
|
|
||||||
|
let ty = IntersectionBuilder::new(&db).build();
|
||||||
|
|
||||||
|
assert_eq!(ty, KnownClass::Object.to_instance(&db));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_intersection_flatten_positive() {
|
fn build_intersection_flatten_positive() {
|
||||||
let db = setup_db();
|
let db = setup_db();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue