[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:
David Peter 2025-03-12 13:20:17 +01:00 committed by GitHub
parent b250304ad3
commit a6572a57c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 136 additions and 11 deletions

View file

@ -1042,6 +1042,132 @@ reveal_type(A.__mro__)
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`
The union of the set of types that `Any` could materialise to is equivalent to `object`. It follows

View file

@ -5065,17 +5065,16 @@ impl<'db> IntersectionType<'db> {
let mut builder = IntersectionBuilder::new(db);
let mut any_unbound = false;
let mut any_possibly_unbound = false;
let mut all_unbound = true;
let mut any_definitely_bound = false;
for ty in self.positive(db) {
let ty_member = transform_fn(ty);
match ty_member {
Symbol::Unbound => {
any_unbound = true;
}
Symbol::Unbound => {}
Symbol::Type(ty_member, member_boundness) => {
if member_boundness == Boundness::PossiblyUnbound {
any_possibly_unbound = true;
all_unbound = false;
if member_boundness == Boundness::Bound {
any_definitely_bound = true;
}
builder = builder.add_positive(ty_member);
@ -5083,15 +5082,15 @@ impl<'db> IntersectionType<'db> {
}
}
if any_unbound {
if all_unbound {
Symbol::Unbound
} else {
Symbol::Type(
builder.build(),
if any_possibly_unbound {
Boundness::PossiblyUnbound
} else {
if any_definitely_bound {
Boundness::Bound
} else {
Boundness::PossiblyUnbound
},
)
}