mirror of https://github.com/astral-sh/ruff
[red-knot] Attribute access on intersection types (#16665)
## Summary
Implements attribute access on intersection types, which didn't
previously work. For example:
```py
from typing import Any
class P: ...
class Q: ...
class A:
x: P = P()
class B:
x: Any = Q()
def _(obj: A):
if isinstance(obj, B):
reveal_type(obj.x) # revealed: P & Any
```
Refers to [this comment].
[this comment]:
https://github.com/astral-sh/ruff/pull/16416#discussion_r1985040363
## Test Plan
New Markdown tests
This commit is contained in:
parent
b250304ad3
commit
a6572a57c4
|
|
@ -1042,6 +1042,132 @@ reveal_type(A.__mro__)
|
||||||
reveal_type(A.X) # revealed: Unknown | Literal[42]
|
reveal_type(A.X) # revealed: Unknown | Literal[42]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Intersections of attributes
|
||||||
|
|
||||||
|
### Attribute only available on one element
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import Intersection
|
||||||
|
|
||||||
|
class A:
|
||||||
|
x: int = 1
|
||||||
|
|
||||||
|
class B: ...
|
||||||
|
|
||||||
|
def _(a_and_b: Intersection[A, B]):
|
||||||
|
reveal_type(a_and_b.x) # revealed: int
|
||||||
|
|
||||||
|
# Same for class objects
|
||||||
|
def _(a_and_b: Intersection[type[A], type[B]]):
|
||||||
|
reveal_type(a_and_b.x) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
### Attribute available on both elements
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import Intersection
|
||||||
|
|
||||||
|
class P: ...
|
||||||
|
class Q: ...
|
||||||
|
|
||||||
|
class A:
|
||||||
|
x: P = P()
|
||||||
|
|
||||||
|
class B:
|
||||||
|
x: Q = Q()
|
||||||
|
|
||||||
|
def _(a_and_b: Intersection[A, B]):
|
||||||
|
reveal_type(a_and_b.x) # revealed: P & Q
|
||||||
|
|
||||||
|
# Same for class objects
|
||||||
|
def _(a_and_b: Intersection[type[A], type[B]]):
|
||||||
|
reveal_type(a_and_b.x) # revealed: P & Q
|
||||||
|
```
|
||||||
|
|
||||||
|
### Possible unboundness
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import Intersection
|
||||||
|
|
||||||
|
class P: ...
|
||||||
|
class Q: ...
|
||||||
|
|
||||||
|
def _(flag: bool):
|
||||||
|
class A1:
|
||||||
|
if flag:
|
||||||
|
x: P = P()
|
||||||
|
|
||||||
|
class B1: ...
|
||||||
|
|
||||||
|
def inner1(a_and_b: Intersection[A1, B1]):
|
||||||
|
# error: [possibly-unbound-attribute]
|
||||||
|
reveal_type(a_and_b.x) # revealed: P
|
||||||
|
# Same for class objects
|
||||||
|
def inner1_class(a_and_b: Intersection[type[A1], type[B1]]):
|
||||||
|
# error: [possibly-unbound-attribute]
|
||||||
|
reveal_type(a_and_b.x) # revealed: P
|
||||||
|
|
||||||
|
class A2:
|
||||||
|
if flag:
|
||||||
|
x: P = P()
|
||||||
|
|
||||||
|
class B1:
|
||||||
|
x: Q = Q()
|
||||||
|
|
||||||
|
def inner2(a_and_b: Intersection[A2, B1]):
|
||||||
|
reveal_type(a_and_b.x) # revealed: P & Q
|
||||||
|
# Same for class objects
|
||||||
|
def inner2_class(a_and_b: Intersection[type[A2], type[B1]]):
|
||||||
|
reveal_type(a_and_b.x) # revealed: P & Q
|
||||||
|
|
||||||
|
class A3:
|
||||||
|
if flag:
|
||||||
|
x: P = P()
|
||||||
|
|
||||||
|
class B3:
|
||||||
|
if flag:
|
||||||
|
x: Q = Q()
|
||||||
|
|
||||||
|
def inner3(a_and_b: Intersection[A3, B3]):
|
||||||
|
# error: [possibly-unbound-attribute]
|
||||||
|
reveal_type(a_and_b.x) # revealed: P & Q
|
||||||
|
# Same for class objects
|
||||||
|
def inner3_class(a_and_b: Intersection[type[A3], type[B3]]):
|
||||||
|
# error: [possibly-unbound-attribute]
|
||||||
|
reveal_type(a_and_b.x) # revealed: P & Q
|
||||||
|
|
||||||
|
class A4: ...
|
||||||
|
class B4: ...
|
||||||
|
|
||||||
|
def inner4(a_and_b: Intersection[A4, B4]):
|
||||||
|
# error: [unresolved-attribute]
|
||||||
|
reveal_type(a_and_b.x) # revealed: Unknown
|
||||||
|
# Same for class objects
|
||||||
|
def inner4_class(a_and_b: Intersection[type[A4], type[B4]]):
|
||||||
|
# error: [unresolved-attribute]
|
||||||
|
reveal_type(a_and_b.x) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
### Intersection of implicit instance attributes
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import Intersection
|
||||||
|
|
||||||
|
class P: ...
|
||||||
|
class Q: ...
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __init__(self):
|
||||||
|
self.x: P = P()
|
||||||
|
|
||||||
|
class B:
|
||||||
|
def __init__(self):
|
||||||
|
self.x: Q = Q()
|
||||||
|
|
||||||
|
def _(a_and_b: Intersection[A, B]):
|
||||||
|
reveal_type(a_and_b.x) # revealed: P & Q
|
||||||
|
```
|
||||||
|
|
||||||
## Attribute access on `Any`
|
## Attribute access on `Any`
|
||||||
|
|
||||||
The union of the set of types that `Any` could materialise to is equivalent to `object`. It follows
|
The union of the set of types that `Any` could materialise to is equivalent to `object`. It follows
|
||||||
|
|
|
||||||
|
|
@ -5065,17 +5065,16 @@ impl<'db> IntersectionType<'db> {
|
||||||
|
|
||||||
let mut builder = IntersectionBuilder::new(db);
|
let mut builder = IntersectionBuilder::new(db);
|
||||||
|
|
||||||
let mut any_unbound = false;
|
let mut all_unbound = true;
|
||||||
let mut any_possibly_unbound = false;
|
let mut any_definitely_bound = false;
|
||||||
for ty in self.positive(db) {
|
for ty in self.positive(db) {
|
||||||
let ty_member = transform_fn(ty);
|
let ty_member = transform_fn(ty);
|
||||||
match ty_member {
|
match ty_member {
|
||||||
Symbol::Unbound => {
|
Symbol::Unbound => {}
|
||||||
any_unbound = true;
|
|
||||||
}
|
|
||||||
Symbol::Type(ty_member, member_boundness) => {
|
Symbol::Type(ty_member, member_boundness) => {
|
||||||
if member_boundness == Boundness::PossiblyUnbound {
|
all_unbound = false;
|
||||||
any_possibly_unbound = true;
|
if member_boundness == Boundness::Bound {
|
||||||
|
any_definitely_bound = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
builder = builder.add_positive(ty_member);
|
builder = builder.add_positive(ty_member);
|
||||||
|
|
@ -5083,15 +5082,15 @@ impl<'db> IntersectionType<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if any_unbound {
|
if all_unbound {
|
||||||
Symbol::Unbound
|
Symbol::Unbound
|
||||||
} else {
|
} else {
|
||||||
Symbol::Type(
|
Symbol::Type(
|
||||||
builder.build(),
|
builder.build(),
|
||||||
if any_possibly_unbound {
|
if any_definitely_bound {
|
||||||
Boundness::PossiblyUnbound
|
|
||||||
} else {
|
|
||||||
Boundness::Bound
|
Boundness::Bound
|
||||||
|
} else {
|
||||||
|
Boundness::PossiblyUnbound
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue