diff --git a/crates/red_knot_python_semantic/resources/mdtest/union_types.md b/crates/red_knot_python_semantic/resources/mdtest/union_types.md index 2fa27ca211..a215a6cff2 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/union_types.md +++ b/crates/red_knot_python_semantic/resources/mdtest/union_types.md @@ -37,6 +37,31 @@ def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) -> None: reveal_type(u2) # revealed: int | str ``` +## `object` subsumes everything + +Unions with `object` can be simplified to `object`: + +```py +from typing_extensions import Never, Any + +def _( + u1: int | object, + u2: object | int, + u3: Any | object, + u4: object | Any, + u5: object | Never, + u6: Never | object, + u7: int | str | object | bytes | Any, +) -> None: + reveal_type(u1) # revealed: object + reveal_type(u2) # revealed: object + reveal_type(u3) # revealed: object + reveal_type(u4) # revealed: object + reveal_type(u5) # revealed: object + reveal_type(u6) # revealed: object + reveal_type(u7) # revealed: object +``` + ## Flattening of nested unions ```py @@ -120,8 +145,8 @@ Simplifications still apply when `Unknown` is present. ```py from knot_extensions import Unknown -def _(u1: str | Unknown | int | object): - reveal_type(u1) # revealed: Unknown | object +def _(u1: int | Unknown | bool) -> None: + reveal_type(u1) # revealed: int | Unknown ``` ## Union of intersections diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index d9d8ac2e9e..6c38c57b46 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -631,6 +631,10 @@ impl<'db> Type<'db> { Self::Dynamic(DynamicType::Unknown) } + pub fn object(db: &'db dyn Db) -> Self { + KnownClass::Object.to_instance(db) + } + pub const fn is_unknown(&self) -> bool { matches!(self, Type::Dynamic(DynamicType::Unknown)) } @@ -639,6 +643,11 @@ impl<'db> Type<'db> { matches!(self, Type::Never) } + pub fn is_object(&self, db: &'db dyn Db) -> bool { + self.into_instance() + .is_some_and(|instance| instance.class.is_object(db)) + } + pub const fn is_todo(&self) -> bool { matches!(self, Type::Dynamic(DynamicType::Todo(_))) } @@ -895,7 +904,7 @@ impl<'db> Type<'db> { // `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) => + if class.is_object(db) => { true } @@ -949,7 +958,7 @@ impl<'db> Type<'db> { (left, Type::AlwaysTruthy) => left.bool(db).is_always_true(), // Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance). (Type::AlwaysFalsy | Type::AlwaysTruthy, _) => { - target.is_equivalent_to(db, KnownClass::Object.to_instance(db)) + target.is_equivalent_to(db, Type::object(db)) } // All `StringLiteral` types are a subtype of `LiteralString`. @@ -1088,11 +1097,7 @@ impl<'db> Type<'db> { // 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 - } + (_, Type::Instance(InstanceType { class })) if class.is_object(db) => true, // A union is assignable to a type T iff every element of the union is assignable to T. (Type::Union(union), ty) => union @@ -1801,7 +1806,7 @@ impl<'db> Type<'db> { // TODO should be `Callable[[], Literal[True/False]]` todo_type!("`__bool__` for `AlwaysTruthy`/`AlwaysFalsy` Type variants").into() } - _ => KnownClass::Object.to_instance(db).member(db, name), + _ => Type::object(db).member(db, name), }, } } @@ -3853,6 +3858,11 @@ impl<'db> Class<'db> { self.known(db) == Some(known_class) } + /// Return `true` if this class represents the builtin class `object` + pub fn is_object(self, db: &'db dyn Db) -> bool { + self.is_known(db, KnownClass::Object) + } + /// Return an iterator over the inferred types of this class's *explicit* bases. /// /// Note that any class (except for `object`) that has no explicit diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index c19a4f06ce..45be6ac488 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -43,6 +43,13 @@ impl<'db> UnionBuilder<'db> { } } + /// Collapse the union to a single type: `object`. + fn collapse_to_object(mut self) -> Self { + self.elements.clear(); + self.elements.push(Type::object(self.db)); + self + } + /// Adds a type to this union. pub(crate) fn add(mut self, ty: Type<'db>) -> Self { match ty { @@ -53,7 +60,12 @@ impl<'db> UnionBuilder<'db> { self = self.add(*element); } } + // Adding `Never` to a union is a no-op. Type::Never => {} + // Adding `object` to a union results in `object`. + ty if ty.is_object(self.db) => { + return self.collapse_to_object(); + } _ => { let bool_pair = if let Type::BooleanLiteral(b) = ty { Some(Type::BooleanLiteral(!b)) @@ -76,7 +88,10 @@ impl<'db> UnionBuilder<'db> { break; } - if ty.is_same_gradual_form(*element) || ty.is_subtype_of(self.db, *element) { + if ty.is_same_gradual_form(*element) + || ty.is_subtype_of(self.db, *element) + || element.is_object(self.db) + { return self; } else if element.is_subtype_of(self.db, ty) { to_remove.push(index); @@ -88,9 +103,7 @@ impl<'db> UnionBuilder<'db> { // `element | ty` must be `object` (object has no other supertypes). This means we can simplify // the whole union to just `object`, since all other potential elements would also be subtypes of // `object`. - self.elements.clear(); - self.elements.push(KnownClass::Object.to_instance(self.db)); - return self; + return self.collapse_to_object(); } } match to_remove[..] { @@ -416,7 +429,7 @@ impl<'db> InnerIntersectionBuilder<'db> { Type::Never => { // Adding ~Never to an intersection is a no-op. } - Type::Instance(instance) if instance.class.is_known(db, KnownClass::Object) => { + Type::Instance(instance) if instance.class.is_object(db) => { // Adding ~object to an intersection results in Never. *self = Self::default(); self.positive.insert(Type::Never); @@ -481,7 +494,7 @@ impl<'db> InnerIntersectionBuilder<'db> { fn build(mut self, db: &'db dyn Db) -> Type<'db> { match (self.positive.len(), self.negative.len()) { - (0, 0) => KnownClass::Object.to_instance(db), + (0, 0) => Type::object(db), (1, 0) => self.positive[0], _ => { self.positive.shrink_to_fit(); @@ -534,7 +547,7 @@ mod tests { let db = setup_db(); let intersection = IntersectionBuilder::new(&db).build(); - assert_eq!(intersection, KnownClass::Object.to_instance(&db)); + assert_eq!(intersection, Type::object(&db)); } #[test_case(Type::BooleanLiteral(true))] @@ -548,7 +561,7 @@ mod tests { // We add t_object in various orders (in first or second position) in // the tests below to ensure that the boolean simplification eliminates // everything from the intersection, not just `bool`. - let t_object = KnownClass::Object.to_instance(&db); + let t_object = Type::object(&db); let t_bool = KnownClass::Bool.to_instance(&db); let ty = IntersectionBuilder::new(&db) diff --git a/crates/red_knot_python_semantic/src/types/mro.rs b/crates/red_knot_python_semantic/src/types/mro.rs index 444335e1ee..90f3f47ade 100644 --- a/crates/red_knot_python_semantic/src/types/mro.rs +++ b/crates/red_knot_python_semantic/src/types/mro.rs @@ -4,7 +4,7 @@ use std::ops::Deref; use rustc_hash::FxHashSet; use crate::types::class_base::ClassBase; -use crate::types::{Class, KnownClass, Type}; +use crate::types::{Class, Type}; use crate::Db; /// The inferred method resolution order of a given class. @@ -52,9 +52,7 @@ impl<'db> Mro<'db> { match class_bases { // `builtins.object` is the special case: // the only class in Python that has an MRO with length <2 - [] if class.is_known(db, KnownClass::Object) => { - Ok(Self::from([ClassBase::Class(class)])) - } + [] if class.is_object(db) => Ok(Self::from([ClassBase::Class(class)])), // All other classes in Python have an MRO with length >=2. // Even if a class has no explicit base classes, diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index 9fef834db4..6b0e9f8973 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -152,13 +152,13 @@ fn merge_constraints_or<'db>( *entry.get_mut() = UnionBuilder::new(db).add(*entry.get()).add(*value).build(); } Entry::Vacant(entry) => { - entry.insert(KnownClass::Object.to_instance(db)); + entry.insert(Type::object(db)); } } } for (key, value) in into.iter_mut() { if !from.contains_key(key) { - *value = KnownClass::Object.to_instance(db); + *value = Type::object(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 5f6e8edf06..9d3db01a76 100644 --- a/crates/red_knot_python_semantic/src/types/property_tests.rs +++ b/crates/red_knot_python_semantic/src/types/property_tests.rs @@ -329,7 +329,7 @@ fn union<'db>(db: &'db TestDb, tys: impl IntoIterator>) -> Type mod stable { use super::union; - use crate::types::{KnownClass, Type}; + use crate::types::Type; // Reflexivity: `T` is equivalent to itself. type_property_test!( @@ -419,13 +419,13 @@ mod stable { // 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)) + forall types t. t.is_assignable_to(db, Type::object(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)) + forall types t. t.is_fully_static(db) => t.is_subtype_of(db, Type::object(db)) ); // Never should be assignable to every type diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 6174730bec..9d44c22bbb 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -411,7 +411,7 @@ mod tests { }, Parameter { name: Some(Name::new_static("args")), - annotated_ty: Some(KnownClass::Object.to_instance(&db)), + annotated_ty: Some(Type::object(&db)), kind: ParameterKind::Variadic, }, Parameter { diff --git a/crates/red_knot_python_semantic/src/types/subclass_of.rs b/crates/red_knot_python_semantic/src/types/subclass_of.rs index fe5cc5cd98..4a0705dfcf 100644 --- a/crates/red_knot_python_semantic/src/types/subclass_of.rs +++ b/crates/red_knot_python_semantic/src/types/subclass_of.rs @@ -26,7 +26,7 @@ impl<'db> SubclassOfType<'db> { ClassBase::Class(class) => { if class.is_final(db) { Type::ClassLiteral(ClassLiteralType { class }) - } else if class.is_known(db, KnownClass::Object) { + } else if class.is_object(db) { KnownClass::Type.to_instance(db) } else { Type::SubclassOf(Self { subclass_of })