diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index f2fce8dd4b..00a88e1e10 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -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 diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 2bda4280c9..7d15f381d6 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -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 }, ) }