From fdca2b422e4d4a4ebcb7ad5b429b935f3f92f56a Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Tue, 7 Jan 2025 15:19:07 -0800 Subject: [PATCH] [red-knot] all types are assignable to object (#15332) ## Summary `Type[Any]` should be assignable to `object`. All types should be assignable to `object`. We specifically didn't understand the former; this PR adds a test for it, and a case to ensure that `Type[Any]` is assignable to anything that `type` is assignable to (which includes `object`). This PR also adds a property test that all types are assignable to object. In order to make it pass, I added a special case to check early if we are assigning to `object` and just return `true`. In principle, once we get all the more general cases correct, this special case might be removable. But having the special case for now allows the property test to pass. And we add a property test that all types are subtypes of object. This failed for the case of an intersection with no positive elements (that is, a negation type). This really does need to be a special case for `object`, because there is no other type we can know that a negation type is a subtype of. ## Test Plan Added unit test and property test. --------- Co-authored-by: Alex Waygood --- crates/red_knot_python_semantic/src/types.rs | 67 ++++++++++++++++--- .../src/types/property_tests.rs | 14 ++++ crates/ruff_benchmark/benches/red_knot.rs | 3 +- 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index d7d2043582..4597563b27 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 } - (Type::SubclassOf(subclass_of_ty), Type::Instance(_)) - if subclass_of_ty.is_dynamic() - && target.is_assignable_to(db, KnownClass::Type.to_instance(db)) => + + // 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() + && (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 c97ec05b4c..e18e8f15fe 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 8abb040dae..d7f59b623e 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`",