diff --git a/crates/ruff_db/src/files.rs b/crates/ruff_db/src/files.rs index 1c322419e0..f58f0013b1 100644 --- a/crates/ruff_db/src/files.rs +++ b/crates/ruff_db/src/files.rs @@ -492,6 +492,10 @@ impl File { .map_or(PySourceType::Python, PySourceType::from_extension), } } + + pub fn structural_ordering(self, db: &dyn Db, other: Self) -> std::cmp::Ordering { + self.path(db).cmp(other.path(db)) + } } impl fmt::Debug for File { diff --git a/crates/ruff_db/src/files/path.rs b/crates/ruff_db/src/files/path.rs index 557a70cdf9..5738c40073 100644 --- a/crates/ruff_db/src/files/path.rs +++ b/crates/ruff_db/src/files/path.rs @@ -11,7 +11,7 @@ use std::fmt::{Display, Formatter}; /// * a file stored on the [host system](crate::system::System). /// * a virtual file stored on the [host system](crate::system::System). /// * a vendored file stored in the [vendored file system](crate::vendored::VendoredFileSystem). -#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, get_size2::GetSize)] pub enum FilePath { /// Path to a file on the [host system](crate::system::System). System(SystemPathBuf), diff --git a/crates/ruff_db/src/vendored/path.rs b/crates/ruff_db/src/vendored/path.rs index 86cdd5057e..d93a4bb978 100644 --- a/crates/ruff_db/src/vendored/path.rs +++ b/crates/ruff_db/src/vendored/path.rs @@ -88,7 +88,7 @@ impl ToOwned for VendoredPath { } #[repr(transparent)] -#[derive(Debug, Eq, PartialEq, Clone, Hash)] +#[derive(Debug, Eq, PartialEq, Clone, Hash, PartialOrd, Ord)] pub struct VendoredPathBuf(Utf8PathBuf); impl get_size2::GetSize for VendoredPathBuf { diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 18a645b31f..5d62823853 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -2410,7 +2410,7 @@ class C3: self.x = [self.x[0].flip()] # TODO: should be `list[Unknown | Sub] | list[Unknown | Base] | Unknown` -reveal_type(C3(Sub()).x) # revealed: list[Unknown | Sub] | list[Divergent] | Unknown +reveal_type(C3(Sub()).x) # revealed: list[Divergent] | list[Unknown | Sub] | Unknown ``` And cycles between many attributes: @@ -2469,7 +2469,7 @@ class ManyCycles2: def f1(self: "ManyCycles2"): # TODO: should be list[Unknown | int] | list[Divergent] | Unknown - reveal_type(self.x3) # revealed: list[Unknown | int] | list[Divergent] | list[Divergent] | Unknown + reveal_type(self.x3) # revealed: list[Divergent] | list[Divergent] | list[Unknown | int] | Unknown self.x1 = [self.x2] + [self.x3] self.x2 = [self.x1] + [self.x3] diff --git a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md index 77c1817ddd..a8dbae5a2e 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -248,9 +248,9 @@ IntOrStr = TypeAliasType(get_name(), int | str) type OptNestedInt = int | tuple[OptNestedInt, ...] | None def f(x: OptNestedInt) -> None: - reveal_type(x) # revealed: int | None | tuple[OptNestedInt, ...] + reveal_type(x) # revealed: tuple[OptNestedInt, ...] | None | int if x is not None: - reveal_type(x) # revealed: int | tuple[OptNestedInt, ...] + reveal_type(x) # revealed: tuple[OptNestedInt, ...] | int ``` ### Invalid self-referential @@ -344,12 +344,12 @@ def f(x: A): reveal_type(y) # revealed: tuple[A] def g(x: A | B): - reveal_type(x) # revealed: None | tuple[B] + reveal_type(x) # revealed: tuple[B] | None from ty_extensions import Intersection def h(x: Intersection[A, B]): - reveal_type(x) # revealed: None | tuple[B] + reveal_type(x) # revealed: tuple[B] | None ``` ### Self-recursive callable type diff --git a/crates/ty_python_semantic/src/module_resolver/module.rs b/crates/ty_python_semantic/src/module_resolver/module.rs index 6927c3b89f..88700e3889 100644 --- a/crates/ty_python_semantic/src/module_resolver/module.rs +++ b/crates/ty_python_semantic/src/module_resolver/module.rs @@ -102,6 +102,17 @@ impl<'db> Module<'db> { .as_deref() .unwrap_or_default() } + + pub fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self, other) { + (Module::File(left), Module::File(right)) => left.structural_ordering(db, right), + (Module::Namespace(left), Module::Namespace(right)) => { + left.name(db).cmp(right.name(db)) + } + (Module::File(_), Module::Namespace(_)) => std::cmp::Ordering::Less, + (Module::Namespace(_), Module::File(_)) => std::cmp::Ordering::Greater, + } + } } impl std::fmt::Debug for Module<'_> { @@ -274,6 +285,14 @@ pub struct FileModule<'db> { pub(super) known: Option, } +impl FileModule<'_> { + pub fn structural_ordering(self, db: &dyn Db, other: Self) -> std::cmp::Ordering { + self.name(db) + .cmp(other.name(db)) + .then_with(|| self.file(db).structural_ordering(db, other.file(db))) + } +} + /// A namespace package. /// /// Namespace packages are special because there are diff --git a/crates/ty_python_semantic/src/semantic_index/definition.rs b/crates/ty_python_semantic/src/semantic_index/definition.rs index 2659e75493..d368ad8017 100644 --- a/crates/ty_python_semantic/src/semantic_index/definition.rs +++ b/crates/ty_python_semantic/src/semantic_index/definition.rs @@ -122,6 +122,17 @@ impl<'db> Definition<'db> { _ => None, } } + + pub(crate) fn structural_ordering( + self, + db: &'db dyn Db, + other: Definition<'db>, + ) -> std::cmp::Ordering { + self.file(db) + .cmp(&other.file(db)) + .then_with(|| self.file_scope(db).cmp(&other.file_scope(db))) + .then_with(|| self.place(db).cmp(&other.place(db))) + } } /// Get the module-level docstring for the given file diff --git a/crates/ty_python_semantic/src/semantic_index/member.rs b/crates/ty_python_semantic/src/semantic_index/member.rs index 8511c7b295..f4d8d1c41b 100644 --- a/crates/ty_python_semantic/src/semantic_index/member.rs +++ b/crates/ty_python_semantic/src/semantic_index/member.rs @@ -347,7 +347,7 @@ impl Hash for MemberExprRef<'_> { /// Uniquely identifies a member in a scope. #[newtype_index] -#[derive(get_size2::GetSize, salsa::Update)] +#[derive(PartialOrd, Ord, get_size2::GetSize, salsa::Update)] pub struct ScopedMemberId; /// The members of a scope. Allows lookup by member path and [`ScopedMemberId`]. diff --git a/crates/ty_python_semantic/src/semantic_index/place.rs b/crates/ty_python_semantic/src/semantic_index/place.rs index 04bea97626..2167462842 100644 --- a/crates/ty_python_semantic/src/semantic_index/place.rs +++ b/crates/ty_python_semantic/src/semantic_index/place.rs @@ -133,7 +133,9 @@ impl std::fmt::Display for PlaceExprRef<'_> { } /// ID that uniquely identifies a place inside a [`Scope`](super::FileScopeId). -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, get_size2::GetSize, salsa::Update)] +#[derive( + Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, get_size2::GetSize, salsa::Update, +)] pub enum ScopedPlaceId { Symbol(ScopedSymbolId), Member(ScopedMemberId), diff --git a/crates/ty_python_semantic/src/semantic_index/scope.rs b/crates/ty_python_semantic/src/semantic_index/scope.rs index c7c42241a3..bb5cb0e580 100644 --- a/crates/ty_python_semantic/src/semantic_index/scope.rs +++ b/crates/ty_python_semantic/src/semantic_index/scope.rs @@ -64,11 +64,21 @@ impl<'db> ScopeId<'db> { NodeWithScopeKind::GeneratorExpression(_) => "", } } + + pub(crate) fn structural_ordering( + self, + db: &'db dyn Db, + other: ScopeId<'db>, + ) -> std::cmp::Ordering { + self.file(db) + .cmp(&other.file(db)) + .then_with(|| self.file_scope_id(db).cmp(&other.file_scope_id(db))) + } } /// ID that uniquely identifies a scope inside of a module. #[newtype_index] -#[derive(salsa::Update, get_size2::GetSize)] +#[derive(salsa::Update, get_size2::GetSize, PartialOrd, Ord)] pub struct FileScopeId; impl FileScopeId { diff --git a/crates/ty_python_semantic/src/semantic_index/symbol.rs b/crates/ty_python_semantic/src/semantic_index/symbol.rs index 8aea606f59..f29734a1ce 100644 --- a/crates/ty_python_semantic/src/semantic_index/symbol.rs +++ b/crates/ty_python_semantic/src/semantic_index/symbol.rs @@ -8,7 +8,7 @@ use std::ops::{Deref, DerefMut}; /// Uniquely identifies a symbol in a given scope. #[newtype_index] -#[derive(get_size2::GetSize)] +#[derive(PartialOrd, Ord, get_size2::GetSize)] pub struct ScopedSymbolId; /// A symbol in a given scope. diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index a4ce2b46ba..df8f6e70e9 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -19,7 +19,7 @@ use ruff_python_ast::name::Name; use ruff_text_size::{Ranged, TextRange}; use smallvec::{SmallVec, smallvec}; -use type_ordering::union_or_intersection_elements_ordering; +use type_ordering::{structural_type_ordering, union_or_intersection_elements_ordering}; pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder}; pub use self::cyclic::CycleDetector; @@ -653,6 +653,26 @@ impl<'db> PropertyInstanceType<'db> { getter_equivalence.and(db, setter_equivalence) } + + fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + let getter_ord = match (self.getter(db), other.getter(db)) { + (Some(left), Some(right)) => structural_type_ordering(db, &left, &right), + (Some(_), None) => std::cmp::Ordering::Greater, + (None, Some(_)) => std::cmp::Ordering::Less, + (None, None) => std::cmp::Ordering::Equal, + }; + + if getter_ord != std::cmp::Ordering::Equal { + return getter_ord; + } + + match (self.setter(db), other.setter(db)) { + (Some(left), Some(right)) => structural_type_ordering(db, &left, &right), + (Some(_), None) => std::cmp::Ordering::Greater, + (None, Some(_)) => std::cmp::Ordering::Less, + (None, None) => std::cmp::Ordering::Equal, + } + } } bitflags! { @@ -756,6 +776,30 @@ impl<'db> DataclassParams<'db> { params.field_specifiers(db), ) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + let flag_ord = self.flags(db).bits().cmp(&other.flags(db).bits()); + if flag_ord != std::cmp::Ordering::Equal { + return flag_ord; + } + + let self_fields = self.field_specifiers(db); + let other_fields = other.field_specifiers(db); + + let fields_count = self_fields.len().cmp(&other_fields.len()); + if fields_count != std::cmp::Ordering::Equal { + return fields_count; + } + + for (self_field, other_field) in self_fields.iter().zip(other_fields.iter()) { + let field_ord = structural_type_ordering(db, self_field, other_field); + if field_ord != std::cmp::Ordering::Equal { + return field_ord; + } + } + + std::cmp::Ordering::Equal + } } /// Representation of a type: a set of possible values at runtime. @@ -8764,6 +8808,61 @@ impl<'db> KnownInstanceType<'db> { fn repr(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { self.display_with(db, DisplaySettings::default()) } + + fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self, other) { + (Self::SubscriptedProtocol(left), Self::SubscriptedProtocol(right)) + | (Self::SubscriptedGeneric(left), Self::SubscriptedGeneric(right)) + | (Self::GenericContext(left), Self::GenericContext(right)) => { + left.structural_ordering(db, right) + } + (Self::TypeVar(left), Self::TypeVar(right)) => left + .identity(db) + .structural_ordering(db, right.identity(db)), + (Self::TypeAliasType(left), Self::TypeAliasType(right)) => { + left.structural_ordering(db, right) + } + (Self::Deprecated(left), Self::Deprecated(right)) => { + left.structural_ordering(db, right) + } + (Self::Field(left), Self::Field(right)) => left.structural_ordering(db, right), + // No need to compare structurally, they are used only in debugging contexts + (Self::ConstraintSet(left), Self::ConstraintSet(right)) => left.cmp(&right), + (Self::Specialization(left), Self::Specialization(right)) => { + left.structural_ordering(db, right) + } + (Self::UnionType(left), Self::UnionType(right)) => left.structural_ordering(db, right), + (Self::Literal(left), Self::Literal(right)) + | (Self::Annotated(left), Self::Annotated(right)) + | (Self::TypeGenericAlias(left), Self::TypeGenericAlias(right)) + | (Self::LiteralStringAlias(left), Self::LiteralStringAlias(right)) => { + structural_type_ordering(db, &left.inner(db), &right.inner(db)) + } + (Self::Callable(left), Self::Callable(right)) => left.structural_ordering(db, right), + (Self::NewType(left), Self::NewType(right)) => left.structural_ordering(db, right), + (left, right) => { + let index = |instance| match instance { + Self::SubscriptedProtocol(_) => 0, + Self::SubscriptedGeneric(_) => 1, + Self::TypeVar(_) => 2, + Self::TypeAliasType(_) => 3, + Self::Deprecated(_) => 4, + Self::Field(_) => 5, + Self::ConstraintSet(_) => 6, + Self::GenericContext(_) => 7, + Self::Specialization(_) => 8, + Self::UnionType(_) => 9, + Self::Literal(_) => 10, + Self::Annotated(_) => 11, + Self::TypeGenericAlias(_) => 12, + Self::Callable(_) => 13, + Self::LiteralStringAlias(_) => 14, + Self::NewType(_) => 15, + }; + index(left).cmp(&index(right)) + } + } + } } /// A type that is determined to be divergent during recursive type inference. @@ -8842,7 +8941,7 @@ impl std::fmt::Display for DynamicType { bitflags! { /// Type qualifiers that appear in an annotation expression. - #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, salsa::Update, Hash)] + #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, salsa::Update, Hash, PartialOrd, Ord)] pub(crate) struct TypeQualifiers: u8 { /// `typing.ClassVar` const CLASS_VAR = 1 << 0; @@ -9161,6 +9260,17 @@ pub struct DeprecatedInstance<'db> { // The Salsa heap is tracked separately. impl get_size2::GetSize for DeprecatedInstance<'_> {} +impl DeprecatedInstance<'_> { + fn structural_ordering(self, db: &dyn Db, other: DeprecatedInstance<'_>) -> std::cmp::Ordering { + match (self.message(db), other.message(db)) { + (Some(left), Some(right)) => left.structural_ordering(db, right), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + } +} + /// Contains information about instances of `dataclasses.Field`, typically created using /// `dataclasses.field()`. #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] @@ -9218,6 +9328,24 @@ impl<'db> FieldInstance<'db> { self.alias(db), )) } + + fn structural_ordering(self, db: &dyn Db, other: FieldInstance<'_>) -> std::cmp::Ordering { + match (self.default_type(db), other.default_type(db)) { + (Some(left), Some(right)) => { + let ord = structural_type_ordering(db, &left, &right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + (None, Some(_)) => return std::cmp::Ordering::Less, + (Some(_), None) => return std::cmp::Ordering::Greater, + (None, None) => {} + } + self.init(db) + .cmp(&other.init(db)) + .then_with(|| self.kw_only(db).cmp(&other.kw_only(db))) + .then_with(|| self.alias(db).cmp(&other.alias(db))) + } } /// Whether this typevar was created via the legacy `TypeVar` constructor, using PEP 695 syntax, @@ -9271,6 +9399,19 @@ pub struct TypeVarIdentity<'db> { impl get_size2::GetSize for TypeVarIdentity<'_> {} +impl TypeVarIdentity<'_> { + fn structural_ordering(self, db: &dyn Db, other: TypeVarIdentity<'_>) -> std::cmp::Ordering { + self.name(db).cmp(other.name(db)).then_with(|| { + match (self.definition(db), other.definition(db)) { + (Some(left), Some(right)) => left.structural_ordering(db, right), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + }) + } +} + /// A specific instance of a type variable that has not been bound to a generic context yet. /// /// This is usually not the type that you want; if you are working with a typevar, in a generic @@ -9857,6 +9998,17 @@ impl<'db> BoundTypeVarInstance<'db> { } } } + + fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + self.typevar(db) + .identity(db) + .structural_ordering(db, other.typevar(db).identity(db)) + .then_with(|| { + self.binding_context(db) + .definition() + .cmp(&other.binding_context(db).definition()) + }) + } } fn walk_bound_type_var_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( @@ -10184,6 +10336,27 @@ impl<'db> UnionTypeInstance<'db> { Some(Self::new(db, value_expr_types, union_type)) } + + fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self._value_expr_types(db), other._value_expr_types(db)) { + (Some(left_tys), Some(right_tys)) => { + let len_count = left_tys.len().cmp(&right_tys.len()); + if len_count != std::cmp::Ordering::Equal { + return len_count; + } + for (left, right) in left_tys.iter().zip(right_tys.iter()) { + let ord = structural_type_ordering(db, left, right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + std::cmp::Ordering::Equal + } + (Some(_), None) => std::cmp::Ordering::Less, + (None, Some(_)) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + } } /// A salsa-interned `Type` @@ -11589,6 +11762,14 @@ impl<'db> BoundMethodType<'db> { ) }) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + self.function(db) + .structural_ordering(db, other.function(db)) + .then_with(|| { + structural_type_ordering(db, &self.self_instance(db), &other.self_instance(db)) + }) + } } /// This type represents the set of all callable objects with a certain, possibly overloaded, @@ -11777,6 +11958,15 @@ impl<'db> CallableType<'db> { .is_equivalent_to_impl(db, other.signatures(db), inferable, visitor) }) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + self.is_function_like(db) + .cmp(&other.is_function_like(db)) + .then_with(|| { + self.signatures(db) + .structural_ordering(db, other.signatures(db)) + }) + } } /// Converting a type "into a callable" can possibly return a _union_ of callables. Eventually, @@ -12391,6 +12581,48 @@ impl<'db> KnownBoundMethodType<'db> { } } } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self, other) { + ( + KnownBoundMethodType::FunctionTypeDunderGet(self_function), + KnownBoundMethodType::FunctionTypeDunderGet(other_function), + ) => self_function.structural_ordering(db, other_function), + + ( + KnownBoundMethodType::FunctionTypeDunderCall(self_function), + KnownBoundMethodType::FunctionTypeDunderCall(other_function), + ) => self_function.structural_ordering(db, other_function), + + ( + KnownBoundMethodType::PropertyDunderGet(self_property), + KnownBoundMethodType::PropertyDunderGet(other_property), + ) => self_property.structural_ordering(db, other_property), + + ( + KnownBoundMethodType::PropertyDunderSet(self_property), + KnownBoundMethodType::PropertyDunderSet(other_property), + ) => self_property.structural_ordering(db, other_property), + + (left, right) => { + let index = |known| match known { + KnownBoundMethodType::FunctionTypeDunderGet(_) => 0, + KnownBoundMethodType::FunctionTypeDunderCall(_) => 1, + KnownBoundMethodType::PropertyDunderGet(_) => 2, + KnownBoundMethodType::PropertyDunderSet(_) => 3, + KnownBoundMethodType::StrStartswith(_) => 4, + KnownBoundMethodType::ConstraintSetRange => 5, + KnownBoundMethodType::ConstraintSetAlways => 6, + KnownBoundMethodType::ConstraintSetNever => 7, + KnownBoundMethodType::ConstraintSetImpliesSubtypeOf(_) => 8, + KnownBoundMethodType::ConstraintSetSatisfies(_) => 9, + KnownBoundMethodType::ConstraintSetSatisfiedByAllTypeVars(_) => 10, + KnownBoundMethodType::GenericContextSpecializeConstrained(_) => 11, + }; + index(left).cmp(&index(right)) + } + } + } } /// Represents a specific instance of `types.WrapperDescriptorType` @@ -12646,6 +12878,14 @@ impl<'db> ModuleLiteralType<'db> { place_and_qualifiers } + + pub(super) fn structural_ordering( + self, + db: &'db dyn Db, + other: ModuleLiteralType<'db>, + ) -> std::cmp::Ordering { + self.module(db).structural_ordering(db, other.module(db)) + } } /// # Ordering @@ -12759,6 +12999,17 @@ impl<'db> PEP695TypeAliasType<'db> { fn normalized_impl(self, _db: &'db dyn Db, _visitor: &NormalizedVisitor<'db>) -> Self { self } + + fn structural_ordering( + self, + db: &'db dyn Db, + other: PEP695TypeAliasType<'db>, + ) -> std::cmp::Ordering { + self.name(db).cmp(other.name(db)).then_with(|| { + self.rhs_scope(db) + .structural_ordering(db, other.rhs_scope(db)) + }) + } } fn generic_context_cycle_initial<'db>( @@ -12832,6 +13083,21 @@ impl<'db> ManualPEP695TypeAliasType<'db> { .recursive_type_normalized_impl(db, div, true)?, )) } + + fn structural_ordering( + self, + db: &'db dyn Db, + other: ManualPEP695TypeAliasType<'db>, + ) -> std::cmp::Ordering { + self.name(db).cmp(other.name(db)).then_with(|| { + match (self.definition(db), other.definition(db)) { + (Some(left), Some(right)) => left.structural_ordering(db, right), + (None, None) => std::cmp::Ordering::Equal, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, Some(_)) => std::cmp::Ordering::Less, + } + }) + } } #[derive( @@ -12952,6 +13218,21 @@ impl<'db> TypeAliasType<'db> { TypeAliasType::ManualPEP695(_) => self, } } + + fn structural_ordering(self, db: &'db dyn Db, other: TypeAliasType<'db>) -> std::cmp::Ordering { + match (self, other) { + (TypeAliasType::PEP695(left), TypeAliasType::PEP695(right)) => { + left.structural_ordering(db, right) + } + (TypeAliasType::ManualPEP695(left), TypeAliasType::ManualPEP695(right)) => { + left.structural_ordering(db, right) + } + (TypeAliasType::PEP695(_), TypeAliasType::ManualPEP695(_)) => std::cmp::Ordering::Less, + (TypeAliasType::ManualPEP695(_), TypeAliasType::PEP695(_)) => { + std::cmp::Ordering::Greater + } + } + } } /// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. @@ -13356,8 +13637,7 @@ impl<'db> IntersectionType<'db> { .map(|ty| ty.normalized_impl(db, visitor)) .collect(); - elements - .sort_unstable_by(|l, r| union_or_intersection_elements_ordering(db, l, r, false)); + elements.sort_unstable_by(|l, r| union_or_intersection_elements_ordering(db, l, r)); elements } @@ -13599,6 +13879,14 @@ impl<'db> StringLiteralType<'db> { pub(crate) fn python_len(self, db: &'db dyn Db) -> usize { self.value(db).chars().count() } + + fn structural_ordering( + self, + db: &'db dyn Db, + other: StringLiteralType<'db>, + ) -> std::cmp::Ordering { + self.value(db).cmp(other.value(db)) + } } /// # Ordering @@ -13618,6 +13906,14 @@ impl<'db> BytesLiteralType<'db> { pub(crate) fn python_len(self, db: &'db dyn Db) -> usize { self.value(db).len() } + + fn structural_ordering( + self, + db: &'db dyn Db, + other: BytesLiteralType<'db>, + ) -> std::cmp::Ordering { + self.value(db).cmp(other.value(db)) + } } /// A singleton type corresponding to a specific enum member. @@ -13646,6 +13942,17 @@ impl<'db> EnumLiteralType<'db> { pub(crate) fn enum_class_instance(self, db: &'db dyn Db) -> Type<'db> { self.enum_class(db).to_non_generic_instance(db) } + + fn structural_ordering( + self, + db: &'db dyn Db, + other: EnumLiteralType<'db>, + ) -> std::cmp::Ordering { + self.name(db).cmp(other.name(db)).then_with(|| { + self.enum_class(db) + .structural_ordering(db, other.enum_class(db)) + }) + } } #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] @@ -13706,6 +14013,23 @@ impl<'db> TypeIsType<'db> { pub(crate) fn is_bound(self, db: &'db dyn Db) -> bool { self.place_info(db).is_some() } + + pub(super) fn structural_ordering( + self, + db: &'db dyn Db, + other: TypeIsType<'db>, + ) -> std::cmp::Ordering { + structural_type_ordering(db, &self.return_type(db), &other.return_type(db)).then_with( + || match (self.place_info(db), other.place_info(db)) { + (Some((left_scope, left_place)), Some((right_scope, right_place))) => left_scope + .structural_ordering(db, right_scope) + .then_with(|| left_place.cmp(&right_place)), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + }, + ) + } } impl<'db> VarianceInferable<'db> for TypeIsType<'db> { diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/builder.rs index 956343ce35..a9936023df 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/builder.rs @@ -41,7 +41,7 @@ use crate::types::enums::{enum_member_literals, enum_metadata}; use crate::types::type_ordering::union_or_intersection_elements_ordering; use crate::types::{ BytesLiteralType, IntersectionType, KnownClass, StringLiteralType, Type, - TypeVarBoundOrConstraints, UnionType, + TypeVarBoundOrConstraints, UnionType, structural_type_ordering, }; use crate::{Db, FxOrderSet}; use rustc_hash::FxHashSet; @@ -613,7 +613,11 @@ impl<'db> UnionBuilder<'db> { } if self.order_elements { types.sort_unstable_by(|l, r| { - union_or_intersection_elements_ordering(self.db, l, r, self.cycle_recovery) + if self.cycle_recovery && self.recursively_defined.is_yes() { + structural_type_ordering(self.db, l, r) + } else { + union_or_intersection_elements_ordering(self.db, l, r) + } }); } match types.len() { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 8ff558aba8..a88cfd4fe4 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -332,6 +332,19 @@ impl<'db> GenericAlias<'db> { pub(super) fn is_typed_dict(self, db: &'db dyn Db) -> bool { self.origin(db).is_typed_dict(db) } + + pub(super) fn structural_ordering( + self, + db: &'db dyn Db, + other: GenericAlias<'db>, + ) -> std::cmp::Ordering { + self.origin(db) + .structural_ordering(db, other.origin(db)) + .then_with(|| { + self.specialization(db) + .structural_ordering(db, other.specialization(db)) + }) + } } impl<'db> From> for Type<'db> { @@ -1278,6 +1291,19 @@ impl<'db> ClassType<'db> { pub(super) fn header_span(self, db: &'db dyn Db) -> Span { self.class_literal(db).0.header_span(db) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self, other) { + (ClassType::NonGeneric(left), ClassType::NonGeneric(right)) => { + left.structural_ordering(db, right) + } + (ClassType::Generic(left), ClassType::Generic(right)) => { + left.structural_ordering(db, right) + } + (ClassType::NonGeneric(_), ClassType::Generic(_)) => std::cmp::Ordering::Less, + (ClassType::Generic(_), ClassType::NonGeneric(_)) => std::cmp::Ordering::Greater, + } + } } fn into_callable_cycle_initial<'db>( @@ -3769,6 +3795,20 @@ impl<'db> ClassLiteral<'db> { pub(super) fn qualified_name(self, db: &'db dyn Db) -> QualifiedClassName<'db> { QualifiedClassName { db, class: self } } + + pub(super) fn structural_ordering( + self, + db: &'db dyn Db, + other: ClassLiteral<'db>, + ) -> std::cmp::Ordering { + self.name(db) + .cmp(other.name(db)) + .then_with(|| self.known(db).cmp(&other.known(db))) + .then_with(|| { + self.body_scope(db) + .structural_ordering(db, other.body_scope(db)) + }) + } } impl<'db> From> for Type<'db> { @@ -4050,7 +4090,7 @@ pub(super) enum DisjointBaseKind { /// Feel free to expand this enum if you ever find yourself using the same class in multiple /// places. /// Note: good candidates are any classes in `[crate::module_resolver::module::KnownModule]` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, get_size2::GetSize)] #[cfg_attr(test, derive(strum_macros::EnumIter))] pub enum KnownClass { // To figure out where an stdlib symbol is defined, you can go into `crates/ty_vendored` diff --git a/crates/ty_python_semantic/src/types/function.rs b/crates/ty_python_semantic/src/types/function.rs index 189520fa52..15069d196b 100644 --- a/crates/ty_python_semantic/src/types/function.rs +++ b/crates/ty_python_semantic/src/types/function.rs @@ -1129,6 +1129,21 @@ impl<'db> FunctionType<'db> { updated_last_definition_signature, )) } + + pub(super) fn structural_ordering( + self, + db: &'db dyn Db, + other: FunctionType<'db>, + ) -> std::cmp::Ordering { + self.name(db).cmp(other.name(db)).then_with(|| { + match (self.updated_signature(db), other.updated_signature(db)) { + (Some(left_sig), Some(right_sig)) => left_sig.structural_ordering(db, right_sig), + (None, None) => std::cmp::Ordering::Equal, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, Some(_)) => std::cmp::Ordering::Less, + } + }) + } } /// Evaluate an `isinstance` call. Return `Truthiness::AlwaysTrue` if we can definitely infer that diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index de357dfbce..d3c72dc173 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -21,7 +21,8 @@ use crate::types::{ FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, KnownInstanceType, MaterializationKind, NormalizedVisitor, Type, TypeContext, TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarIdentity, TypeVarInstance, - TypeVarKind, TypeVarVariance, UnionType, declaration_type, walk_bound_type_var_type, + TypeVarKind, TypeVarVariance, UnionType, declaration_type, structural_type_ordering, + walk_bound_type_var_type, }; use crate::{Db, FxOrderMap, FxOrderSet}; @@ -602,6 +603,26 @@ impl<'db> GenericContext<'db> { ) -> usize { ruff_memory_usage::order_map_heap_size(variables) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + let left = self.variables_inner(db); + let right = other.variables_inner(db); + let variables_count = left.len().cmp(&right.len()); + if variables_count != std::cmp::Ordering::Equal { + return variables_count; + } + for (left_key, left_value) in left { + if let Some(right_value) = right.get(left_key) { + let ord = left_value.structural_ordering(db, *right_value); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } else { + return std::cmp::Ordering::Greater; + } + } + std::cmp::Ordering::Equal + } } fn inferable_typevars_cycle_initial<'db>( @@ -1282,6 +1303,20 @@ impl<'db> Specialization<'db> { // A tuple's specialization will include all of its element types, so we don't need to also // look in `self.tuple`. } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + let types_count = self.types(db).len().cmp(&other.types(db).len()); + if types_count != std::cmp::Ordering::Equal { + return types_count; + } + for (left, right) in self.types(db).iter().zip(other.types(db)) { + let ord = structural_type_ordering(db, left, right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + std::cmp::Ordering::Equal + } } /// A mapping between type variables and types. diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index fb53f10ef4..802f38944c 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -547,6 +547,25 @@ impl<'db> NominalInstanceType<'db> { NominalInstanceInner::Object => {} } } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self.0, other.0) { + (NominalInstanceInner::Object, NominalInstanceInner::Object) => { + std::cmp::Ordering::Equal + } + (NominalInstanceInner::Object, _) => std::cmp::Ordering::Less, + (_, NominalInstanceInner::Object) => std::cmp::Ordering::Greater, + ( + NominalInstanceInner::ExactTuple(tuple1), + NominalInstanceInner::ExactTuple(tuple2), + ) => tuple1.structural_ordering(db, tuple2), + (NominalInstanceInner::ExactTuple(_), _) => std::cmp::Ordering::Less, + (_, NominalInstanceInner::ExactTuple(_)) => std::cmp::Ordering::Greater, + (NominalInstanceInner::NonTuple(class1), NominalInstanceInner::NonTuple(class2)) => { + class1.structural_ordering(db, class2) + } + } + } } impl<'db> From> for Type<'db> { @@ -835,6 +854,19 @@ impl<'db> ProtocolInstanceType<'db> { pub(super) fn interface(self, db: &'db dyn Db) -> ProtocolInterface<'db> { self.inner.interface(db) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + match (self.inner, other.inner) { + (Protocol::FromClass(left), Protocol::FromClass(right)) => { + left.structural_ordering(db, *right) + } + (Protocol::Synthesized(left), Protocol::Synthesized(right)) => { + left.structural_ordering(db, right) + } + (Protocol::FromClass(_), Protocol::Synthesized(_)) => std::cmp::Ordering::Less, + (Protocol::Synthesized(_), Protocol::FromClass(_)) => std::cmp::Ordering::Greater, + } + } } impl<'db> VarianceInferable<'db> for ProtocolInstanceType<'db> { @@ -963,6 +995,14 @@ mod synthesized_protocol { self.0.recursive_type_normalized_impl(db, div, nested)?, )) } + + pub(in crate::types) fn structural_ordering( + self, + db: &'db dyn Db, + other: Self, + ) -> std::cmp::Ordering { + self.0.structural_ordering(db, other.0) + } } impl<'db> VarianceInferable<'db> for SynthesizedProtocolType<'db> { diff --git a/crates/ty_python_semantic/src/types/newtype.rs b/crates/ty_python_semantic/src/types/newtype.rs index 84a6e18f50..48bb1df20f 100644 --- a/crates/ty_python_semantic/src/types/newtype.rs +++ b/crates/ty_python_semantic/src/types/newtype.rs @@ -194,6 +194,13 @@ impl<'db> NewType<'db> { self.try_map_base_class_type(db, |class_type| Some(f(class_type))) .unwrap() } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + self.name(db).cmp(other.name(db)).then_with(|| { + self.definition(db) + .structural_ordering(db, other.definition(db)) + }) + } } pub(crate) fn walk_newtype_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( diff --git a/crates/ty_python_semantic/src/types/protocol_class.rs b/crates/ty_python_semantic/src/types/protocol_class.rs index 862349ea40..286c6e1f56 100644 --- a/crates/ty_python_semantic/src/types/protocol_class.rs +++ b/crates/ty_python_semantic/src/types/protocol_class.rs @@ -6,7 +6,7 @@ use itertools::Itertools; use ruff_python_ast::name::Name; use rustc_hash::FxHashMap; -use crate::types::TypeContext; +use crate::types::{TypeContext, structural_type_ordering}; use crate::{ Db, FxOrderSet, place::{Definedness, Place, PlaceAndQualifiers, place_from_bindings, place_from_declarations}, @@ -461,6 +461,22 @@ impl<'db> ProtocolInterface<'db> { interface: self, } } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + let members_count = self.members(db).len().cmp(&other.members(db).len()); + if members_count != std::cmp::Ordering::Equal { + return members_count; + } + + for (left, right) in self.members(db).zip(other.members(db)) { + let ord = left.structural_ordering(db, &right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + + std::cmp::Ordering::Equal + } } impl<'db> VarianceInferable<'db> for ProtocolInterface<'db> { @@ -813,6 +829,42 @@ impl<'a, 'db> ProtocolMember<'a, 'db> { } } } + + fn structural_ordering(&self, db: &'db dyn Db, other: &Self) -> std::cmp::Ordering { + let name_ordering = self.name.cmp(other.name); + if name_ordering != std::cmp::Ordering::Equal { + return name_ordering; + } + let qualifiers_ordering = self.qualifiers.cmp(&other.qualifiers); + if qualifiers_ordering != std::cmp::Ordering::Equal { + return qualifiers_ordering; + } + + match (&self.kind, &other.kind) { + (ProtocolMemberKind::Method(left), ProtocolMemberKind::Method(right)) => { + left.structural_ordering(db, *right) + } + (ProtocolMemberKind::Property(left), ProtocolMemberKind::Property(right)) => { + left.structural_ordering(db, *right) + } + (ProtocolMemberKind::Other(left), ProtocolMemberKind::Other(right)) => { + structural_type_ordering(db, left, right) + } + (left, right) => { + let left_index = match left { + ProtocolMemberKind::Method(_) => 0, + ProtocolMemberKind::Property(_) => 1, + ProtocolMemberKind::Other(_) => 2, + }; + let right_index = match right { + ProtocolMemberKind::Method(_) => 0, + ProtocolMemberKind::Property(_) => 1, + ProtocolMemberKind::Other(_) => 2, + }; + left_index.cmp(&right_index) + } + } + } } /// Returns `true` if a declaration or binding to a given name in a protocol class body diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 738fcfb971..b4cb81bda6 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -31,7 +31,8 @@ use crate::types::infer::nearest_enclosing_class; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassLiteral, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, KnownClass, MaterializationKind, - NormalizedVisitor, TypeContext, TypeMapping, TypeRelation, VarianceInferable, todo_type, + NormalizedVisitor, TypeContext, TypeMapping, TypeRelation, VarianceInferable, + structural_type_ordering, todo_type, }; use crate::{Db, FxOrderSet}; use ruff_python_ast::{self as ast, name::Name}; @@ -367,6 +368,23 @@ impl<'db> CallableSignature<'db> { } } } + + pub(super) fn structural_ordering( + &self, + db: &'db dyn Db, + other: &CallableSignature<'db>, + ) -> std::cmp::Ordering { + if self.overloads.len() != other.overloads.len() { + return self.overloads.len().cmp(&other.overloads.len()); + } + for (left_sig, right_sig) in self.overloads.iter().zip(&other.overloads) { + let ord = left_sig.structural_ordering(db, right_sig); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + std::cmp::Ordering::Equal + } } impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> { @@ -1294,6 +1312,26 @@ impl<'db> Signature<'db> { pub(crate) fn with_definition(self, definition: Option>) -> Self { Self { definition, ..self } } + + fn structural_ordering(&self, db: &'db dyn Db, other: &Signature<'db>) -> std::cmp::Ordering { + let parameters_count = self.parameters.len().cmp(&other.parameters.len()); + if parameters_count != std::cmp::Ordering::Equal { + return parameters_count; + } + for (left, right) in self.parameters.iter().zip(&other.parameters) { + let ord = left.structural_ordering(db, right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + + match (self.return_ty.as_ref(), other.return_ty.as_ref()) { + (Some(left_ty), Some(right_ty)) => structural_type_ordering(db, left_ty, right_ty), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + } } impl<'db> VarianceInferable<'db> for &Signature<'db> { @@ -2074,6 +2112,17 @@ impl<'db> Parameter<'db> { ParameterKind::Variadic { .. } | ParameterKind::KeywordVariadic { .. } => None, } } + + fn structural_ordering(&self, db: &'db dyn Db, other: &Parameter<'db>) -> std::cmp::Ordering { + self.kind.cmp(&other.kind).then_with(|| { + match (self.annotated_type.as_ref(), other.annotated_type.as_ref()) { + (Some(left_ty), Some(right_ty)) => structural_type_ordering(db, left_ty, right_ty), + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (None, None) => std::cmp::Ordering::Equal, + } + }) + } } #[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update, get_size2::GetSize)] @@ -2115,6 +2164,46 @@ pub(crate) enum ParameterKind<'db> { }, } +impl Ord for ParameterKind<'_> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match (self, other) { + ( + Self::PositionalOnly { name: l_name, .. }, + Self::PositionalOnly { name: r_name, .. }, + ) => l_name.cmp(r_name), + ( + Self::PositionalOrKeyword { name: l_name, .. }, + Self::PositionalOrKeyword { name: r_name, .. }, + ) => l_name.cmp(r_name), + (Self::Variadic { name: l_name }, Self::Variadic { name: r_name }) => { + l_name.cmp(r_name) + } + (Self::KeywordOnly { name: l_name, .. }, Self::KeywordOnly { name: r_name, .. }) => { + l_name.cmp(r_name) + } + (Self::KeywordVariadic { name: l_name }, Self::KeywordVariadic { name: r_name }) => { + l_name.cmp(r_name) + } + (left, right) => { + let index = |param: &_| match param { + Self::PositionalOnly { .. } => 0, + Self::PositionalOrKeyword { .. } => 1, + Self::Variadic { .. } => 2, + Self::KeywordOnly { .. } => 3, + Self::KeywordVariadic { .. } => 4, + }; + index(left).cmp(&index(right)) + } + } + } +} + +impl PartialOrd for ParameterKind<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl<'db> ParameterKind<'db> { fn apply_type_mapping_impl<'a>( &self, diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 6405ae3ae7..577abc3f73 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -30,7 +30,7 @@ use crate::types::generics::InferableTypeVars; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, IsEquivalentVisitor, NormalizedVisitor, Type, TypeMapping, TypeRelation, - UnionBuilder, UnionType, + UnionBuilder, UnionType, structural_type_ordering, }; use crate::types::{Truthiness, TypeContext}; use crate::{Db, FxOrderSet, Program}; @@ -302,6 +302,10 @@ impl<'db> TupleType<'db> { pub(crate) fn is_single_valued(self, db: &'db dyn Db) -> bool { self.tuple(db).is_single_valued(db) } + + pub(super) fn structural_ordering(self, db: &'db dyn Db, other: Self) -> std::cmp::Ordering { + self.tuple(db).structural_ordering(db, other.tuple(db)) + } } fn to_class_type_cycle_initial<'db>( @@ -583,6 +587,22 @@ impl<'db> FixedLengthTuple> { fn is_single_valued(&self, db: &'db dyn Db) -> bool { self.0.iter().all(|ty| ty.is_single_valued(db)) } + + fn structural_ordering(&self, db: &'db dyn Db, other: &Self) -> std::cmp::Ordering { + let len_count = self.0.len().cmp(&other.0.len()); + if len_count != std::cmp::Ordering::Equal { + return len_count; + } + + for (left, right) in self.0.iter().zip(&other.0) { + let ord = structural_type_ordering(db, left, right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + + std::cmp::Ordering::Equal + } } impl<'db> PyIndex<'db> for &FixedLengthTuple> { @@ -1096,6 +1116,32 @@ impl<'db> VariableLengthTuple> { }) }) } + + fn structural_ordering(&self, db: &'db dyn Db, other: &Self) -> std::cmp::Ordering { + let prefix_count = self.prefix.len().cmp(&other.prefix.len()); + if prefix_count != std::cmp::Ordering::Equal { + return prefix_count; + } + let suffix_count = self.suffix.len().cmp(&other.suffix.len()); + if suffix_count != std::cmp::Ordering::Equal { + return suffix_count; + } + + for (left, right) in self.prefix.iter().zip(&other.prefix) { + let ord = structural_type_ordering(db, left, right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + for (left, right) in self.suffix.iter().zip(&other.suffix) { + let ord = structural_type_ordering(db, left, right); + if ord != std::cmp::Ordering::Equal { + return ord; + } + } + + structural_type_ordering(db, &self.variable, &other.variable) + } } impl<'db> PyIndex<'db> for &VariableLengthTuple> { @@ -1470,6 +1516,19 @@ impl<'db> Tuple> { int_instance_ty, ]) } + + pub(super) fn structural_ordering(&self, db: &'db dyn Db, other: &Self) -> std::cmp::Ordering { + match (self, other) { + (Tuple::Fixed(self_tuple), Tuple::Fixed(other_tuple)) => { + self_tuple.structural_ordering(db, other_tuple) + } + (Tuple::Variable(self_tuple), Tuple::Variable(other_tuple)) => { + self_tuple.structural_ordering(db, other_tuple) + } + (Tuple::Fixed(_), Tuple::Variable(_)) => std::cmp::Ordering::Less, + (Tuple::Variable(_), Tuple::Fixed(_)) => std::cmp::Ordering::Greater, + } + } } impl From> for Tuple { diff --git a/crates/ty_python_semantic/src/types/type_ordering.rs b/crates/ty_python_semantic/src/types/type_ordering.rs index 66c10e87d5..0b90fe7644 100644 --- a/crates/ty_python_semantic/src/types/type_ordering.rs +++ b/crates/ty_python_semantic/src/types/type_ordering.rs @@ -8,6 +8,262 @@ use super::{ DynamicType, TodoType, Type, TypeIsType, class_base::ClassBase, subclass_of::SubclassOfInner, }; +pub(super) fn structural_type_ordering<'db>( + db: &'db dyn Db, + left: &Type<'db>, + right: &Type<'db>, +) -> Ordering { + match (left, right) { + (Type::Never, _) => Ordering::Less, + (_, Type::Never) => Ordering::Greater, + + (Type::LiteralString, _) => Ordering::Less, + (_, Type::LiteralString) => Ordering::Greater, + + (Type::BooleanLiteral(left), Type::BooleanLiteral(right)) => left.cmp(right), + (Type::BooleanLiteral(_), _) => Ordering::Less, + (_, Type::BooleanLiteral(_)) => Ordering::Greater, + + (Type::IntLiteral(left), Type::IntLiteral(right)) => left.cmp(right), + (Type::IntLiteral(_), _) => Ordering::Less, + (_, Type::IntLiteral(_)) => Ordering::Greater, + + (Type::StringLiteral(left), Type::StringLiteral(right)) => { + left.structural_ordering(db, *right) + } + (Type::StringLiteral(_), _) => Ordering::Less, + (_, Type::StringLiteral(_)) => Ordering::Greater, + + (Type::BytesLiteral(left), Type::BytesLiteral(right)) => { + left.structural_ordering(db, *right) + } + (Type::BytesLiteral(_), _) => Ordering::Less, + (_, Type::BytesLiteral(_)) => Ordering::Greater, + + (Type::EnumLiteral(left), Type::EnumLiteral(right)) => left.structural_ordering(db, *right), + (Type::EnumLiteral(_), _) => Ordering::Less, + (_, Type::EnumLiteral(_)) => Ordering::Greater, + + (Type::FunctionLiteral(left), Type::FunctionLiteral(right)) => { + left.structural_ordering(db, *right) + } + (Type::FunctionLiteral(_), _) => Ordering::Less, + (_, Type::FunctionLiteral(_)) => Ordering::Greater, + + (Type::BoundMethod(left), Type::BoundMethod(right)) => left.structural_ordering(db, *right), + (Type::BoundMethod(_), _) => Ordering::Less, + (_, Type::BoundMethod(_)) => Ordering::Greater, + + (Type::KnownBoundMethod(left), Type::KnownBoundMethod(right)) => { + left.structural_ordering(db, *right) + } + (Type::KnownBoundMethod(_), _) => Ordering::Less, + (_, Type::KnownBoundMethod(_)) => Ordering::Greater, + + (Type::WrapperDescriptor(left), Type::WrapperDescriptor(right)) => left.cmp(right), + (Type::WrapperDescriptor(_), _) => Ordering::Less, + (_, Type::WrapperDescriptor(_)) => Ordering::Greater, + + (Type::DataclassDecorator(left), Type::DataclassDecorator(right)) => { + left.structural_ordering(db, *right) + } + (Type::DataclassDecorator(_), _) => Ordering::Less, + (_, Type::DataclassDecorator(_)) => Ordering::Greater, + + (Type::DataclassTransformer(left), Type::DataclassTransformer(right)) => left.cmp(right), + (Type::DataclassTransformer(_), _) => Ordering::Less, + (_, Type::DataclassTransformer(_)) => Ordering::Greater, + + (Type::Callable(left), Type::Callable(right)) => left.structural_ordering(db, *right), + (Type::Callable(_), _) => Ordering::Less, + (_, Type::Callable(_)) => Ordering::Greater, + + (Type::ModuleLiteral(left), Type::ModuleLiteral(right)) => { + left.structural_ordering(db, *right) + } + (Type::ModuleLiteral(_), _) => Ordering::Less, + (_, Type::ModuleLiteral(_)) => Ordering::Greater, + + (Type::ClassLiteral(left), Type::ClassLiteral(right)) => { + left.structural_ordering(db, *right) + } + (Type::ClassLiteral(_), _) => Ordering::Less, + (_, Type::ClassLiteral(_)) => Ordering::Greater, + + (Type::GenericAlias(left), Type::GenericAlias(right)) => { + left.structural_ordering(db, *right) + } + (Type::GenericAlias(_), _) => Ordering::Less, + (_, Type::GenericAlias(_)) => Ordering::Greater, + + (Type::SubclassOf(left), Type::SubclassOf(right)) => { + match (left.subclass_of(), right.subclass_of()) { + (SubclassOfInner::Class(left), SubclassOfInner::Class(right)) => { + left.structural_ordering(db, right) + } + (SubclassOfInner::Class(_), _) => Ordering::Less, + (_, SubclassOfInner::Class(_)) => Ordering::Greater, + (SubclassOfInner::Dynamic(left), SubclassOfInner::Dynamic(right)) => { + dynamic_elements_ordering(left, right) + } + (SubclassOfInner::TypeVar(left), SubclassOfInner::TypeVar(right)) => { + left.structural_ordering(db, right) + } + (SubclassOfInner::TypeVar(_), _) => Ordering::Less, + (_, SubclassOfInner::TypeVar(_)) => Ordering::Greater, + } + } + + (Type::SubclassOf(_), _) => Ordering::Less, + (_, Type::SubclassOf(_)) => Ordering::Greater, + + (Type::TypeIs(left), Type::TypeIs(right)) => left.structural_ordering(db, *right), + (Type::TypeIs(_), _) => Ordering::Less, + (_, Type::TypeIs(_)) => Ordering::Greater, + + (Type::NominalInstance(left), Type::NominalInstance(right)) => { + left.structural_ordering(db, *right) + } + (Type::NominalInstance(_), _) => Ordering::Less, + (_, Type::NominalInstance(_)) => Ordering::Greater, + + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { + left.structural_ordering(db, *right) + } + (Type::ProtocolInstance(_), _) => Ordering::Less, + (_, Type::ProtocolInstance(_)) => Ordering::Greater, + + (Type::TypeVar(left), Type::TypeVar(right)) => left.structural_ordering(db, *right), + (Type::TypeVar(_), _) => Ordering::Less, + (_, Type::TypeVar(_)) => Ordering::Greater, + + (Type::AlwaysTruthy, _) => Ordering::Less, + (_, Type::AlwaysTruthy) => Ordering::Greater, + + (Type::AlwaysFalsy, _) => Ordering::Less, + (_, Type::AlwaysFalsy) => Ordering::Greater, + + (Type::BoundSuper(left), Type::BoundSuper(right)) => { + (match (left.pivot_class(db), right.pivot_class(db)) { + (ClassBase::Class(left), ClassBase::Class(right)) => { + left.structural_ordering(db, right) + } + (ClassBase::Class(_), _) => Ordering::Less, + (_, ClassBase::Class(_)) => Ordering::Greater, + + (ClassBase::Protocol, _) => Ordering::Less, + (_, ClassBase::Protocol) => Ordering::Greater, + + (ClassBase::Generic, _) => Ordering::Less, + (_, ClassBase::Generic) => Ordering::Greater, + + (ClassBase::TypedDict, _) => Ordering::Less, + (_, ClassBase::TypedDict) => Ordering::Greater, + + (ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => { + dynamic_elements_ordering(left, right) + } + }) + .then_with(|| match (left.owner(db), right.owner(db)) { + (SuperOwnerKind::Class(left), SuperOwnerKind::Class(right)) => { + left.structural_ordering(db, right) + } + (SuperOwnerKind::Class(_), _) => Ordering::Less, + (_, SuperOwnerKind::Class(_)) => Ordering::Greater, + (SuperOwnerKind::Instance(left), SuperOwnerKind::Instance(right)) => { + left.structural_ordering(db, right) + } + (SuperOwnerKind::Instance(_), _) => Ordering::Less, + (_, SuperOwnerKind::Instance(_)) => Ordering::Greater, + (SuperOwnerKind::Dynamic(left), SuperOwnerKind::Dynamic(right)) => { + dynamic_elements_ordering(left, right) + } + }) + } + (Type::BoundSuper(_), _) => Ordering::Less, + (_, Type::BoundSuper(_)) => Ordering::Greater, + + (Type::SpecialForm(left), Type::SpecialForm(right)) => left.cmp(right), + (Type::SpecialForm(_), _) => Ordering::Less, + (_, Type::SpecialForm(_)) => Ordering::Greater, + + (Type::KnownInstance(left), Type::KnownInstance(right)) => { + left.structural_ordering(db, *right) + } + (Type::KnownInstance(_), _) => Ordering::Less, + (_, Type::KnownInstance(_)) => Ordering::Greater, + + (Type::PropertyInstance(left), Type::PropertyInstance(right)) => { + left.structural_ordering(db, *right) + } + (Type::PropertyInstance(_), _) => Ordering::Less, + (_, Type::PropertyInstance(_)) => Ordering::Greater, + + (Type::Dynamic(left), Type::Dynamic(right)) => dynamic_elements_ordering(*left, *right), + (Type::Dynamic(_), _) => Ordering::Less, + (_, Type::Dynamic(_)) => Ordering::Greater, + + (Type::TypeAlias(left), Type::TypeAlias(right)) => left.structural_ordering(db, *right), + (Type::TypeAlias(_), _) => Ordering::Less, + (_, Type::TypeAlias(_)) => Ordering::Greater, + + (Type::TypedDict(left), Type::TypedDict(right)) => left + .defining_class() + .structural_ordering(db, right.defining_class()), + (Type::TypedDict(_), _) => Ordering::Less, + (_, Type::TypedDict(_)) => Ordering::Greater, + + (Type::NewTypeInstance(left), Type::NewTypeInstance(right)) => { + left.structural_ordering(db, *right) + } + (Type::NewTypeInstance(_), _) => Ordering::Less, + (_, Type::NewTypeInstance(_)) => Ordering::Greater, + + (Type::Union(left), Type::Union(right)) => { + if left.elements(db).len() != right.elements(db).len() { + return left.elements(db).len().cmp(&right.elements(db).len()); + } + for (left, right) in left.elements(db).iter().zip(right.elements(db)) { + let ordering = structural_type_ordering(db, left, right); + if ordering != Ordering::Equal { + return ordering; + } + } + Ordering::Equal + } + (Type::Union(_), _) => Ordering::Less, + (_, Type::Union(_)) => Ordering::Greater, + + (Type::Intersection(left), Type::Intersection(right)) => { + // Lexicographically compare the elements of the two unequal intersections. + let left_positive = left.positive(db); + let right_positive = right.positive(db); + if left_positive.len() != right_positive.len() { + return left_positive.len().cmp(&right_positive.len()); + } + let left_negative = left.negative(db); + let right_negative = right.negative(db); + if left_negative.len() != right_negative.len() { + return left_negative.len().cmp(&right_negative.len()); + } + for (left, right) in left_positive.iter().zip(right_positive) { + let ordering = structural_type_ordering(db, left, right); + if ordering != Ordering::Equal { + return ordering; + } + } + for (left, right) in left_negative.iter().zip(right_negative) { + let ordering = structural_type_ordering(db, left, right); + if ordering != Ordering::Equal { + return ordering; + } + } + + Ordering::Equal + } + } +} + /// Return an [`Ordering`] that describes the canonical order in which two types should appear /// in an [`crate::types::IntersectionType`] or a [`crate::types::UnionType`] in order for them /// to be compared for equivalence. @@ -26,22 +282,17 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( db: &'db dyn Db, left: &Type<'db>, right: &Type<'db>, - cycle_recovery: bool, ) -> Ordering { - // If we sort union types in a cycle recovery function, this check is not necessary - // because the purpose is to stabilize the output and the sort order itself is not important. - if !cycle_recovery { - debug_assert_eq!( - *left, - left.normalized(db), - "`left` must be normalized before a meaningful ordering can be established" - ); - debug_assert_eq!( - *right, - right.normalized(db), - "`right` must be normalized before a meaningful ordering can be established" - ); - } + debug_assert_eq!( + *left, + left.normalized(db), + "`left` must be normalized before a meaningful ordering can be established" + ); + debug_assert_eq!( + *right, + right.normalized(db), + "`right` must be normalized before a meaningful ordering can be established" + ); if left == right { return Ordering::Equal; @@ -133,9 +384,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::SubclassOf(_), _) => Ordering::Less, (_, Type::SubclassOf(_)) => Ordering::Greater, - (Type::TypeIs(left), Type::TypeIs(right)) => { - typeis_ordering(db, *left, *right, cycle_recovery) - } + (Type::TypeIs(left), Type::TypeIs(right)) => typeis_ordering(db, *left, *right), (Type::TypeIs(_), _) => Ordering::Less, (_, Type::TypeIs(_)) => Ordering::Greater, @@ -246,15 +495,13 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( return left_negative.len().cmp(&right_negative.len()); } for (left, right) in left_positive.iter().zip(right_positive) { - let ordering = - union_or_intersection_elements_ordering(db, left, right, cycle_recovery); + let ordering = union_or_intersection_elements_ordering(db, left, right); if ordering != Ordering::Equal { return ordering; } } for (left, right) in left_negative.iter().zip(right_negative) { - let ordering = - union_or_intersection_elements_ordering(db, left, right, cycle_recovery); + let ordering = union_or_intersection_elements_ordering(db, left, right); if ordering != Ordering::Equal { return ordering; } @@ -295,26 +542,17 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering /// * Boundness: Unbound precedes bound /// * Symbol name: String comparison /// * Guarded type: [`union_or_intersection_elements_ordering`] -fn typeis_ordering( - db: &dyn Db, - left: TypeIsType, - right: TypeIsType, - cycle_recovery: bool, -) -> Ordering { +fn typeis_ordering(db: &dyn Db, left: TypeIsType, right: TypeIsType) -> Ordering { let (left_ty, right_ty) = (left.return_type(db), right.return_type(db)); match (left.place_info(db), right.place_info(db)) { (None, Some(_)) => Ordering::Less, (Some(_), None) => Ordering::Greater, - (None, None) => { - union_or_intersection_elements_ordering(db, &left_ty, &right_ty, cycle_recovery) - } + (None, None) => union_or_intersection_elements_ordering(db, &left_ty, &right_ty), (Some(_), Some(_)) => match left.place_name(db).cmp(&right.place_name(db)) { - Ordering::Equal => { - union_or_intersection_elements_ordering(db, &left_ty, &right_ty, cycle_recovery) - } + Ordering::Equal => union_or_intersection_elements_ordering(db, &left_ty, &right_ty), ordering => ordering, }, }