diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index d7d2043582119..4597563b278e6 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -769,6 +769,14 @@ impl<'db> Type<'db> { .iter() .any(|&elem_ty| self.is_subtype_of(db, elem_ty)), + // `object` is the only type that can be known to be a supertype of any intersection, + // even an intersection with no positive elements + (Type::Intersection(_), Type::Instance(InstanceType { class })) + if class.is_known(db, KnownClass::Object) => + { + true + } + (Type::Intersection(self_intersection), Type::Intersection(target_intersection)) => { // Check that all target positive values are covered in self positive values target_intersection @@ -954,16 +962,32 @@ impl<'db> Type<'db> { return true; } match (self, target) { + // The dynamic type is assignable-to and assignable-from any type. (Type::Unknown | Type::Any | Type::Todo(_), _) => true, (_, Type::Unknown | Type::Any | Type::Todo(_)) => true, + + // All types are assignable to `object`. + // TODO this special case might be removable once the below cases are comprehensive + (_, Type::Instance(InstanceType { class })) + if class.is_known(db, KnownClass::Object) => + { + true + } + + // A union is assignable to a type T iff every element of the union is assignable to T. (Type::Union(union), ty) => union .elements(db) .iter() .all(|&elem_ty| elem_ty.is_assignable_to(db, ty)), + + // A type T is assignable to a union iff T is assignable to any element of the union. (ty, Type::Union(union)) => union .elements(db) .iter() .any(|&elem_ty| ty.is_assignable_to(db, elem_ty)), + + // A tuple type S is assignable to a tuple type T if their lengths are the same, and + // each element of S is assignable to the corresponding element of T. (Type::Tuple(self_tuple), Type::Tuple(target_tuple)) => { let self_elements = self_tuple.elements(db); let target_elements = target_tuple.elements(db); @@ -974,28 +998,53 @@ impl<'db> Type<'db> { }, ) } + + // `type[Any]` is assignable to any `type[...]` type, because `type[Any]` can + // materialize to any `type[...]` type. (Type::SubclassOf(subclass_of_ty), Type::SubclassOf(_)) if subclass_of_ty.is_dynamic() => { true } + + // All `type[...]` types are assignable to `type[Any]`, because `type[Any]` can + // materialize to any `type[...]` type. + // + // Every class literal type is also assignable to `type[Any]`, because the class + // literal type for a class `C` is a subtype of `type[C]`, and `type[C]` is assignable + // to `type[Any]`. + (Type::ClassLiteral(_) | Type::SubclassOf(_), Type::SubclassOf(target_subclass_of)) + if target_subclass_of.is_dynamic() => + { + true + } + + // `type[Any]` is assignable to any type that `type[object]` is assignable to, because + // `type[Any]` can materialize to `type[object]`. + // + // `type[Any]` is also assignable to any subtype of `type[object]`, because all + // subtypes of `type[object]` are `type[...]` types (or `Never`), and `type[Any]` can + // materialize to any `type[...]` type (or to `type[Never]`, which is equivalent to + // `Never`.) (Type::SubclassOf(subclass_of_ty), Type::Instance(_)) if subclass_of_ty.is_dynamic() - && target.is_assignable_to(db, KnownClass::Type.to_instance(db)) => + && (KnownClass::Type + .to_instance(db) + .is_assignable_to(db, target) + || target.is_subtype_of(db, KnownClass::Type.to_instance(db))) => { true } + + // Any type that is assignable to `type[object]` is also assignable to `type[Any]`, + // because `type[Any]` can materialize to `type[object]`. (Type::Instance(_), Type::SubclassOf(subclass_of_ty)) if subclass_of_ty.is_dynamic() && self.is_assignable_to(db, KnownClass::Type.to_instance(db)) => { true } - (Type::ClassLiteral(_) | Type::SubclassOf(_), Type::SubclassOf(target_subclass_of)) - if target_subclass_of.is_dynamic() => - { - true - } + // TODO other types containing gradual forms (e.g. generics containing Any/Unknown) _ => self.is_subtype_of(db, target), } @@ -3893,6 +3942,7 @@ pub(crate) mod tests { #[test_case(Ty::SubclassOfUnknown, Ty::SubclassOfBuiltinClass("str"))] #[test_case(Ty::SubclassOfAny, Ty::AbcInstance("ABCMeta"))] #[test_case(Ty::SubclassOfUnknown, Ty::AbcInstance("ABCMeta"))] + #[test_case(Ty::SubclassOfAny, Ty::BuiltinInstance("object"))] fn is_assignable_to(from: Ty, to: Ty) { let db = setup_db(); assert!(from.into_type(&db).is_assignable_to(&db, to.into_type(&db))); @@ -3976,6 +4026,7 @@ pub(crate) mod tests { #[test_case(Ty::Never, Ty::AlwaysTruthy)] #[test_case(Ty::Never, Ty::AlwaysFalsy)] #[test_case(Ty::BuiltinClassLiteral("bool"), Ty::SubclassOfBuiltinClass("int"))] + #[test_case(Ty::Intersection{pos: vec![], neg: vec![Ty::LiteralString]}, Ty::BuiltinInstance("object"))] fn is_subtype_of(from: Ty, to: Ty) { let db = setup_db(); assert!(from.into_type(&db).is_subtype_of(&db, to.into_type(&db))); diff --git a/crates/red_knot_python_semantic/src/types/property_tests.rs b/crates/red_knot_python_semantic/src/types/property_tests.rs index c97ec05b4c44b..e18e8f15fe128 100644 --- a/crates/red_knot_python_semantic/src/types/property_tests.rs +++ b/crates/red_knot_python_semantic/src/types/property_tests.rs @@ -220,6 +220,8 @@ macro_rules! type_property_test { } mod stable { + use super::KnownClass; + // `T` is equivalent to itself. type_property_test!( equivalent_to_is_reflexive, db, @@ -285,6 +287,18 @@ mod stable { non_fully_static_types_do_not_participate_in_subtyping, db, forall types s, t. !s.is_fully_static(db) => !s.is_subtype_of(db, t) && !t.is_subtype_of(db, s) ); + + // All types should be assignable to `object` + type_property_test!( + all_types_assignable_to_object, db, + forall types t. t.is_assignable_to(db, KnownClass::Object.to_instance(db)) + ); + + // And for fully static types, they should also be subtypes of `object` + type_property_test!( + all_fully_static_types_subtype_of_object, db, + forall types t. t.is_fully_static(db) => t.is_subtype_of(db, KnownClass::Object.to_instance(db)) + ); } /// This module contains property tests that currently lead to many false positives. diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/red_knot.rs index 8abb040daefee..d7f59b623ee39 100644 --- a/crates/ruff_benchmark/benches/red_knot.rs +++ b/crates/ruff_benchmark/benches/red_knot.rs @@ -33,8 +33,6 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[ "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined", - // We don't handle intersections in `is_assignable_to` yet - "error[lint:invalid-argument-type] /src/tomllib/_parser.py:211:31 Object of type `Unknown & object | @Todo` cannot be assigned to parameter 1 (`obj`) of function `isinstance`; expected type `object`", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined", @@ -44,6 +42,7 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[ "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined", + // We don't handle intersections in `is_assignable_to` yet "error[lint:invalid-argument-type] /src/tomllib/_parser.py:626:46 Object of type `@Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_datetime`; expected type `Match`", "warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined", "error[lint:invalid-argument-type] /src/tomllib/_parser.py:632:58 Object of type `@Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_localtime`; expected type `Match`",