mirror of https://github.com/astral-sh/ruff
[red-knot] simplify gradually-equivalent types out of unions and intersections (#17467)
## Summary If two types are gradually-equivalent, that means they share the same set of possible materializations. There's no need to keep two such types in the same union or intersection; we should simplify them. Fixes https://github.com/astral-sh/ruff/issues/17465 The one downside here is that now we will simplify e.g. `Unknown | Todo(...)` to just `Unknown`, if `Unknown` was added to the union first. This is correct from a type perspective (they are equivalent types), but it can mean we lose visibility into part of the cause for the type inferring as unknown. I think this is OK, but if we think it's important to avoid this, I can add a special case to try to preserve `Todo` over `Unknown`, if we see them both in the same union or intersection. ## Test Plan Added and updated mdtests.
This commit is contained in:
parent
8fe2dd5e03
commit
2a478ce1b2
|
|
@ -302,7 +302,7 @@ class C:
|
|||
|
||||
c_instance = C()
|
||||
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
|
||||
reveal_type(c_instance.b) # revealed: Unknown | @Todo(starred unpacking)
|
||||
reveal_type(c_instance.b) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Attributes defined in for-loop (unpacking)
|
||||
|
|
@ -1892,6 +1892,17 @@ reveal_type(B().x) # revealed: Unknown | Literal[1]
|
|||
reveal_type(A().x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
This case additionally tests our union/intersection simplification logic:
|
||||
|
||||
```py
|
||||
class H:
|
||||
def __init__(self):
|
||||
self.x = 1
|
||||
|
||||
def copy(self, other: "H"):
|
||||
self.x = other.x or self.x
|
||||
```
|
||||
|
||||
### Builtin types attributes
|
||||
|
||||
This test can probably be removed eventually, but we currently include it because we do not yet
|
||||
|
|
|
|||
|
|
@ -201,3 +201,15 @@ def _(literals_2: Literal[0, 1], b: bool, flag: bool):
|
|||
# Now union the two:
|
||||
reveal_type(bool_and_literals_128 if flag else literals_128_shifted) # revealed: int
|
||||
```
|
||||
|
||||
## Simplifying gradually-equivalent types
|
||||
|
||||
If two types are gradually equivalent, we can keep just one of them in a union:
|
||||
|
||||
```py
|
||||
from typing import Any, Union
|
||||
from knot_extensions import Intersection, Not
|
||||
|
||||
def _(x: Union[Intersection[Any, Not[int]], Intersection[Any, Not[int]]]):
|
||||
reveal_type(x) # revealed: Any & ~int
|
||||
```
|
||||
|
|
|
|||
|
|
@ -842,7 +842,7 @@ def unknown(
|
|||
|
||||
### Mixed dynamic types
|
||||
|
||||
We currently do not simplify mixed dynamic types, but might consider doing so in the future:
|
||||
Gradually-equivalent types can be simplified out of intersections:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
|
@ -854,10 +854,10 @@ def mixed(
|
|||
i3: Intersection[Not[Any], Unknown],
|
||||
i4: Intersection[Not[Any], Not[Unknown]],
|
||||
) -> None:
|
||||
reveal_type(i1) # revealed: Any & Unknown
|
||||
reveal_type(i2) # revealed: Any & Unknown
|
||||
reveal_type(i3) # revealed: Any & Unknown
|
||||
reveal_type(i4) # revealed: Any & Unknown
|
||||
reveal_type(i1) # revealed: Any
|
||||
reveal_type(i2) # revealed: Any
|
||||
reveal_type(i3) # revealed: Any
|
||||
reveal_type(i4) # revealed: Any
|
||||
```
|
||||
|
||||
## Invalid
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ x = [1, 2, 3]
|
|||
reveal_type(x) # revealed: list
|
||||
|
||||
# TODO reveal int
|
||||
reveal_type(x[0]) # revealed: Unknown | @Todo(Support for `typing.TypeVar` instances in type expressions)
|
||||
reveal_type(x[0]) # revealed: Unknown
|
||||
|
||||
# TODO reveal list
|
||||
reveal_type(x[0:1]) # revealed: @Todo(specialized non-generic class)
|
||||
|
|
|
|||
|
|
@ -47,10 +47,7 @@ static_assert(is_gradual_equivalent_to(Intersection[str | int, Not[type[Any]]],
|
|||
static_assert(not is_gradual_equivalent_to(str | int, int | str | bytes))
|
||||
static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict))
|
||||
|
||||
# TODO: No errors
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(Unknown, Unknown | Any))
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(Unknown, Intersection[Unknown, Any]))
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1441,24 +1441,6 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns true if both `self` and `other` are the same gradual form
|
||||
/// (limited to `Any`, `Unknown`, or `Todo`).
|
||||
pub(crate) fn is_same_gradual_form(self, other: Type<'db>) -> bool {
|
||||
matches!(
|
||||
(self, other),
|
||||
(
|
||||
Type::Dynamic(DynamicType::Any),
|
||||
Type::Dynamic(DynamicType::Any)
|
||||
) | (
|
||||
Type::Dynamic(DynamicType::Unknown),
|
||||
Type::Dynamic(DynamicType::Unknown)
|
||||
) | (
|
||||
Type::Dynamic(DynamicType::Todo(_)),
|
||||
Type::Dynamic(DynamicType::Todo(_))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if this type and `other` are gradual equivalent.
|
||||
///
|
||||
/// > Two gradual types `A` and `B` are equivalent
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ impl<'db> UnionBuilder<'db> {
|
|||
break;
|
||||
}
|
||||
|
||||
if ty.is_same_gradual_form(element_type)
|
||||
if ty.is_gradual_equivalent_to(self.db, element_type)
|
||||
|| ty.is_subtype_of(self.db, element_type)
|
||||
|| element_type.is_object(self.db)
|
||||
{
|
||||
|
|
@ -560,7 +560,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||
for (index, existing_positive) in self.positive.iter().enumerate() {
|
||||
// S & T = S if S <: T
|
||||
if existing_positive.is_subtype_of(db, new_positive)
|
||||
|| existing_positive.is_same_gradual_form(new_positive)
|
||||
|| existing_positive.is_gradual_equivalent_to(db, new_positive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
|
@ -656,7 +656,9 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||
let mut to_remove = SmallVec::<[usize; 1]>::new();
|
||||
for (index, existing_negative) in self.negative.iter().enumerate() {
|
||||
// ~S & ~T = ~T if S <: T
|
||||
if existing_negative.is_subtype_of(db, new_negative) {
|
||||
if existing_negative.is_subtype_of(db, new_negative)
|
||||
|| existing_negative.is_gradual_equivalent_to(db, new_negative)
|
||||
{
|
||||
to_remove.push(index);
|
||||
}
|
||||
// same rule, reverse order
|
||||
|
|
|
|||
Loading…
Reference in New Issue