diff --git a/crates/ty_python_semantic/resources/mdtest/properties.md b/crates/ty_python_semantic/resources/mdtest/properties.md index f0f88ae050f34..918757dd83d0e 100644 --- a/crates/ty_python_semantic/resources/mdtest/properties.md +++ b/crates/ty_python_semantic/resources/mdtest/properties.md @@ -136,6 +136,36 @@ c.my_property = 2 c.my_property = "a" ``` +## Conditional redefinition in class body + +Distinct property definitions in statically unknown class-body branches should remain distinct, the +same way methods do: + +```py +from random import random + +class Baz: + if random(): + def method(self) -> int: + return 42 + + @property + def prop(self) -> int: + return 42 + + else: + def method(self) -> str: + return "hello" + + @property + def prop(self) -> str: + return "hello" + +baz = Baz() +reveal_type(baz.prop) # revealed: int | str +reveal_type(baz.method()) # revealed: int | str +``` + ## Failure cases ### Attempting to write to a read-only property @@ -351,3 +381,156 @@ static_assert(not is_subtype_of(TypeOf[attr_property.__set__], types.WrapperDesc static_assert(not is_subtype_of(TypeOf[attr_property.__get__], types.BuiltinMethodType)) static_assert(not is_subtype_of(TypeOf[attr_property.__set__], types.BuiltinMethodType)) ``` + +## Property type relations + +Property equivalence and disjointness are structural over the getter and setter types. We use +standalone property objects here so `TypeOf[...]` sees the raw property type rather than the +`Unknown | ...` that can arise from class-attribute lookup. For the subtype cases, we construct +properties through helper functions with `Callable`-typed parameters so the slot types are +structural rather than exact function literals: + +```py +from typing import Callable +from ty_extensions import ( + CallableTypeOf, + TypeOf, + is_assignable_to, + is_disjoint_from, + is_equivalent_to, + is_subtype_of, + static_assert, +) + +def get_int(self) -> int: + return 1 + +def get_str(self) -> str: + return "a" + +def set_int(self, value: int) -> None: + pass + +def set_object(self, value: object) -> None: + pass + +def set_str(self, value: str) -> None: + pass + +def get_equiv_a(self, /) -> int: + return 1 + +def get_equiv_b(other, /) -> int: + return 1 + +GetterReturnsInt = Callable[[object], int] +GetterReturnsObject = Callable[[object], object] +SetterAcceptsInt = Callable[[object, int], None] +SetterAcceptsObject = Callable[[object, object], None] + +# Use `CallableTypeOf[...]` here rather than plain `Callable[...]` so these getters remain +# equivalent as types while still carrying distinct callable metadata and distinct Salsa IDs. +def assert_equivalent_properties( + getter_a: CallableTypeOf[get_equiv_a], + getter_b: CallableTypeOf[get_equiv_b], +): + getter_only_equivalent_a = property(getter_a) + getter_only_equivalent_b = property(getter_b) + + static_assert(is_equivalent_to(TypeOf[getter_only_equivalent_a], TypeOf[getter_only_equivalent_b])) + static_assert(not is_disjoint_from(TypeOf[getter_only_equivalent_a], TypeOf[getter_only_equivalent_b])) + +def assert_structural_property_relations( + getter_sub: GetterReturnsInt, + getter_super: GetterReturnsObject, + setter_sub: SetterAcceptsObject, + setter_super: SetterAcceptsInt, +): + getter_covariant_sub = property(getter_sub) + getter_covariant_super = property(getter_super) + + setter_contravariant_sub = property(fset=setter_sub) + setter_contravariant_super = property(fset=setter_super) + + both_structural_sub = property(getter_sub, setter_sub) + both_structural_super = property(getter_super, setter_super) + + static_assert(not is_equivalent_to(TypeOf[getter_covariant_sub], TypeOf[getter_covariant_super])) + static_assert(not is_equivalent_to(TypeOf[setter_contravariant_sub], TypeOf[setter_contravariant_super])) + static_assert(not is_equivalent_to(TypeOf[both_structural_sub], TypeOf[both_structural_super])) + + static_assert(is_subtype_of(TypeOf[getter_covariant_sub], TypeOf[getter_covariant_super])) + static_assert(not is_subtype_of(TypeOf[getter_covariant_super], TypeOf[getter_covariant_sub])) + static_assert(is_assignable_to(TypeOf[getter_covariant_sub], TypeOf[getter_covariant_super])) + static_assert(not is_assignable_to(TypeOf[getter_covariant_super], TypeOf[getter_covariant_sub])) + + static_assert(is_subtype_of(TypeOf[setter_contravariant_sub], TypeOf[setter_contravariant_super])) + static_assert(not is_subtype_of(TypeOf[setter_contravariant_super], TypeOf[setter_contravariant_sub])) + static_assert(is_assignable_to(TypeOf[setter_contravariant_sub], TypeOf[setter_contravariant_super])) + static_assert(not is_assignable_to(TypeOf[setter_contravariant_super], TypeOf[setter_contravariant_sub])) + + static_assert(is_subtype_of(TypeOf[both_structural_sub], TypeOf[both_structural_super])) + static_assert(not is_subtype_of(TypeOf[both_structural_super], TypeOf[both_structural_sub])) + static_assert(is_assignable_to(TypeOf[both_structural_sub], TypeOf[both_structural_super])) + static_assert(not is_assignable_to(TypeOf[both_structural_super], TypeOf[both_structural_sub])) + + static_assert(is_subtype_of(TypeOf[both_structural_sub.__get__], TypeOf[both_structural_super.__get__])) + static_assert(not is_subtype_of(TypeOf[both_structural_super.__get__], TypeOf[both_structural_sub.__get__])) + static_assert(is_subtype_of(TypeOf[both_structural_sub.__set__], TypeOf[both_structural_super.__set__])) + static_assert(not is_subtype_of(TypeOf[both_structural_super.__set__], TypeOf[both_structural_sub.__set__])) + + static_assert(not is_disjoint_from(TypeOf[getter_covariant_sub], TypeOf[getter_covariant_super])) + static_assert(not is_disjoint_from(TypeOf[setter_contravariant_sub], TypeOf[setter_contravariant_super])) + static_assert(not is_disjoint_from(TypeOf[both_structural_sub], TypeOf[both_structural_super])) + static_assert(not is_disjoint_from(TypeOf[both_structural_sub.__get__], TypeOf[both_structural_super.__get__])) + static_assert(not is_disjoint_from(TypeOf[both_structural_sub.__set__], TypeOf[both_structural_super.__set__])) + +empty_a = property() +empty_b = property() + +getter_only_a = property(get_int) +getter_only_b = property(get_int) +getter_only_c = property(get_str) + +setter_only_a = property(fset=set_int) +setter_only_b = property(fset=set_int) +setter_only_c = property(fset=set_str) + +both_a = property(get_int, set_int) +both_b = property(get_int, set_int) +both_c = property(get_int, set_str) +both_d = property(get_str, set_int) + +static_assert(is_equivalent_to(TypeOf[empty_a], TypeOf[empty_b])) +static_assert(is_equivalent_to(TypeOf[getter_only_a], TypeOf[getter_only_b])) +static_assert(is_equivalent_to(TypeOf[setter_only_a], TypeOf[setter_only_b])) +static_assert(is_equivalent_to(TypeOf[both_a], TypeOf[both_b])) + +static_assert(not is_equivalent_to(TypeOf[empty_a], TypeOf[getter_only_a])) +static_assert(not is_equivalent_to(TypeOf[empty_a], TypeOf[setter_only_a])) +static_assert(not is_equivalent_to(TypeOf[getter_only_a], TypeOf[getter_only_c])) +static_assert(not is_equivalent_to(TypeOf[getter_only_a], TypeOf[setter_only_a])) +static_assert(not is_equivalent_to(TypeOf[getter_only_a], TypeOf[both_a])) +static_assert(not is_equivalent_to(TypeOf[setter_only_a], TypeOf[setter_only_c])) +static_assert(not is_equivalent_to(TypeOf[setter_only_a], TypeOf[both_a])) +static_assert(not is_equivalent_to(TypeOf[both_a], TypeOf[both_c])) +static_assert(not is_equivalent_to(TypeOf[both_a], TypeOf[both_d])) + +static_assert(not is_disjoint_from(TypeOf[empty_a], TypeOf[empty_b])) +static_assert(not is_disjoint_from(TypeOf[getter_only_a], TypeOf[getter_only_b])) +static_assert(not is_disjoint_from(TypeOf[setter_only_a], TypeOf[setter_only_b])) +static_assert(not is_disjoint_from(TypeOf[both_a], TypeOf[both_b])) + +static_assert(is_disjoint_from(TypeOf[empty_a], TypeOf[getter_only_a])) +static_assert(is_disjoint_from(TypeOf[empty_a], TypeOf[setter_only_a])) +static_assert(is_disjoint_from(TypeOf[getter_only_a], TypeOf[getter_only_c])) +static_assert(is_disjoint_from(TypeOf[getter_only_a], TypeOf[setter_only_a])) +static_assert(is_disjoint_from(TypeOf[getter_only_a], TypeOf[both_a])) +static_assert(is_disjoint_from(TypeOf[setter_only_a], TypeOf[setter_only_c])) +static_assert(is_disjoint_from(TypeOf[setter_only_a], TypeOf[both_a])) +static_assert(is_disjoint_from(TypeOf[both_a], TypeOf[both_c])) +static_assert(is_disjoint_from(TypeOf[both_a], TypeOf[both_d])) + +assert_equivalent_properties(get_equiv_a, get_equiv_b) +assert_structural_property_relations(get_int, get_int, set_object, set_object) +``` diff --git a/crates/ty_python_semantic/src/types/method.rs b/crates/ty_python_semantic/src/types/method.rs index e2d7462194142..ae12232bf494c 100644 --- a/crates/ty_python_semantic/src/types/method.rs +++ b/crates/ty_python_semantic/src/types/method.rs @@ -253,10 +253,12 @@ impl<'db> KnownBoundMethodType<'db> { | ( KnownBoundMethodType::PropertyDunderSet(self_property), KnownBoundMethodType::PropertyDunderSet(other_property), - ) => Type::PropertyInstance(self_property).when_equivalent_to_impl( + ) => Type::PropertyInstance(self_property).has_relation_to_impl( db, Type::PropertyInstance(other_property), constraints, + inferable, + relation, relation_visitor, disjointness_visitor, ), diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index e5f8a2b329d3d..b07ca0835b413 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -10,9 +10,9 @@ use crate::types::cyclic::PairVisitor; use crate::types::enums::is_single_member_enum; use crate::types::set_theoretic::RecursivelyDefined; use crate::types::{ - CallableType, ClassBase, ClassType, CycleDetector, DynamicType, KnownClass, KnownInstanceType, - LiteralValueTypeKind, MemberLookupPolicy, ProtocolInstanceType, SubclassOfInner, - TypeVarBoundOrConstraints, UnionType, UpcastPolicy, + CallableType, ClassBase, ClassType, CycleDetector, DynamicType, KnownBoundMethodType, + KnownClass, KnownInstanceType, LiteralValueTypeKind, MemberLookupPolicy, PropertyInstanceType, + ProtocolInstanceType, SubclassOfInner, TypeVarBoundOrConstraints, UnionType, UpcastPolicy, }; use crate::{ Db, @@ -225,6 +225,121 @@ impl TypeRelation { } } +#[expect(clippy::too_many_arguments)] +fn optional_property_method_has_relation<'db, 'c>( + db: &'db dyn Db, + left: Option>, + right: Option>, + constraints: &'c ConstraintSetBuilder<'db>, + inferable: InferableTypeVars<'_, 'db>, + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, +) -> ConstraintSet<'db, 'c> { + match (left, right) { + (None, None) => ConstraintSet::from_bool(constraints, true), + (Some(left), Some(right)) => left.has_relation_to_impl( + db, + right, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ), + (None | Some(_), None | Some(_)) => ConstraintSet::from_bool(constraints, false), + } +} + +fn optional_property_method_is_disjoint<'db, 'c>( + db: &'db dyn Db, + left: Option>, + right: Option>, + constraints: &'c ConstraintSetBuilder<'db>, + inferable: InferableTypeVars<'_, 'db>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, +) -> ConstraintSet<'db, 'c> { + match (left, right) { + (None, None) => ConstraintSet::from_bool(constraints, false), + (Some(left), Some(right)) => left.is_disjoint_from_impl( + db, + right, + constraints, + inferable, + disjointness_visitor, + relation_visitor, + ), + (None | Some(_), None | Some(_)) => ConstraintSet::from_bool(constraints, true), + } +} + +#[expect(clippy::too_many_arguments)] +fn property_instance_has_relation<'db, 'c>( + db: &'db dyn Db, + left: PropertyInstanceType<'db>, + right: PropertyInstanceType<'db>, + constraints: &'c ConstraintSetBuilder<'db>, + inferable: InferableTypeVars<'_, 'db>, + relation: TypeRelation, + relation_visitor: &HasRelationToVisitor<'db, 'c>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, +) -> ConstraintSet<'db, 'c> { + optional_property_method_has_relation( + db, + left.getter(db), + right.getter(db), + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + .and(db, constraints, || { + optional_property_method_has_relation( + db, + left.setter(db), + right.setter(db), + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) +} + +fn property_instance_is_disjoint<'db, 'c>( + db: &'db dyn Db, + left: PropertyInstanceType<'db>, + right: PropertyInstanceType<'db>, + constraints: &'c ConstraintSetBuilder<'db>, + inferable: InferableTypeVars<'_, 'db>, + disjointness_visitor: &IsDisjointVisitor<'db, 'c>, + relation_visitor: &HasRelationToVisitor<'db, 'c>, +) -> ConstraintSet<'db, 'c> { + optional_property_method_is_disjoint( + db, + left.getter(db), + right.getter(db), + constraints, + inferable, + disjointness_visitor, + relation_visitor, + ) + .or(db, constraints, || { + optional_property_method_is_disjoint( + db, + left.setter(db), + right.setter(db), + constraints, + inferable, + disjointness_visitor, + relation_visitor, + ) + }) +} + #[salsa::tracked] impl<'db> Type<'db> { /// Return `true` if subtyping is always reflexive for this type; `T <: T` is always true for @@ -241,7 +356,17 @@ impl<'db> Type<'db> { | Type::FunctionLiteral(..) | Type::BoundMethod(_) | Type::WrapperDescriptor(_) - | Type::KnownBoundMethod(_) + | Type::KnownBoundMethod( + KnownBoundMethodType::FunctionTypeDunderGet(_) + | KnownBoundMethodType::FunctionTypeDunderCall(_) + | KnownBoundMethodType::StrStartswith(_) + | KnownBoundMethodType::ConstraintSetRange + | KnownBoundMethodType::ConstraintSetAlways + | KnownBoundMethodType::ConstraintSetNever + | KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) + | KnownBoundMethodType::ConstraintSetSatisfies(_) + | KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_), + ) | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) | Type::ModuleLiteral(..) @@ -250,7 +375,6 @@ impl<'db> Type<'db> { | Type::KnownInstance(_) | Type::AlwaysFalsy | Type::AlwaysTruthy - | Type::PropertyInstance(_) // `T` is always a subtype of itself, // and `T` is always a subtype of `T | None` | Type::TypeVar(_) @@ -265,6 +389,11 @@ impl<'db> Type<'db> { | Type::Union(_) | Type::Intersection(_) | Type::Callable(_) + | Type::KnownBoundMethod( + KnownBoundMethodType::PropertyDunderGet(_) + | KnownBoundMethodType::PropertyDunderSet(_), + ) + | Type::PropertyInstance(_) | Type::BoundSuper(_) | Type::TypeIs(_) | Type::TypeGuard(_) @@ -1763,6 +1892,21 @@ impl<'db> Type<'db> { }) } + (Type::PropertyInstance(self_property), Type::PropertyInstance(target_property)) => { + relation_visitor.visit((self, target, relation), || { + property_instance_has_relation( + db, + self_property, + target_property, + constraints, + inferable, + relation, + relation_visitor, + disjointness_visitor, + ) + }) + } + (Type::PropertyInstance(_), _) => { KnownClass::Property.to_instance(db).has_relation_to_impl( db, @@ -2179,6 +2323,35 @@ impl<'db> Type<'db> { ConstraintSet::from_bool(constraints, left.kind() != right.kind()) } + (Type::PropertyInstance(left), Type::PropertyInstance(right)) => { + property_instance_is_disjoint( + db, + left, + right, + constraints, + inferable, + disjointness_visitor, + relation_visitor, + ) + } + + ( + Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderGet(left)), + Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderGet(right)), + ) + | ( + Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderSet(left)), + Type::KnownBoundMethod(KnownBoundMethodType::PropertyDunderSet(right)), + ) => property_instance_is_disjoint( + db, + left, + right, + constraints, + inferable, + disjointness_visitor, + relation_visitor, + ), + // any single-valued type is disjoint from another single-valued type // iff the two types are nonequal (