From 60b486abce4e756fbd26a586a03d551ba44b9ea8 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 20 May 2025 11:41:26 -0400 Subject: [PATCH] [ty] Deeply normalize many types (#18222) --- crates/ty_python_semantic/src/types.rs | 185 +++++++++++++++--- .../src/types/class_base.rs | 13 ++ .../ty_python_semantic/src/types/generics.rs | 19 ++ .../src/types/known_instance.rs | 47 +++++ .../src/types/subclass_of.rs | 13 ++ 5 files changed, 255 insertions(+), 22 deletions(-) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 95246a5121..2823a00022 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -357,6 +357,14 @@ impl<'db> PropertyInstanceType<'db> { Self::new(db, getter, setter) } + fn normalized(self, db: &'db dyn Db) -> Self { + Self::new( + db, + self.getter(db).map(|ty| ty.normalized(db)), + self.setter(db).map(|ty| ty.normalized(db)), + ) + } + fn find_legacy_typevars( self, db: &'db dyn Db, @@ -1005,28 +1013,17 @@ impl<'db> Type<'db> { Type::Callable(callable) => Type::Callable(callable.normalized(db)), Type::ProtocolInstance(protocol) => protocol.normalized(db), Type::NominalInstance(instance) => Type::NominalInstance(instance.normalized(db)), - Type::Dynamic(_) => Type::any(), - Type::LiteralString - | Type::PropertyInstance(_) - | Type::AlwaysFalsy - | Type::AlwaysTruthy - | Type::BooleanLiteral(_) - | Type::BytesLiteral(_) - | Type::StringLiteral(_) - | Type::Never - | Type::FunctionLiteral(_) - | Type::MethodWrapper(_) - | Type::BoundMethod(_) - | Type::WrapperDescriptor(_) - | Type::DataclassDecorator(_) - | Type::DataclassTransformer(_) - | Type::ModuleLiteral(_) - | Type::ClassLiteral(_) - | Type::KnownInstance(_) - | Type::IntLiteral(_) - | Type::BoundSuper(_) - | Type::SubclassOf(_) => self, + Type::Dynamic(dynamic) => Type::Dynamic(dynamic.normalized()), + Type::FunctionLiteral(function) => Type::FunctionLiteral(function.normalized(db)), + Type::PropertyInstance(property) => Type::PropertyInstance(property.normalized(db)), + Type::MethodWrapper(method_kind) => Type::MethodWrapper(method_kind.normalized(db)), + Type::BoundMethod(method) => Type::BoundMethod(method.normalized(db)), + Type::BoundSuper(bound_super) => Type::BoundSuper(bound_super.normalized(db)), Type::GenericAlias(generic) => Type::GenericAlias(generic.normalized(db)), + Type::SubclassOf(subclass_of) => Type::SubclassOf(subclass_of.normalized(db)), + Type::KnownInstance(known_instance) => { + Type::KnownInstance(known_instance.normalized(db)) + } Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { Type::TypeVar(TypeVarInstance::new( @@ -1052,6 +1049,19 @@ impl<'db> Type<'db> { } None => self, }, + Type::LiteralString + | Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::BooleanLiteral(_) + | Type::BytesLiteral(_) + | Type::StringLiteral(_) + | Type::Never + | Type::WrapperDescriptor(_) + | Type::DataclassDecorator(_) + | Type::DataclassTransformer(_) + | Type::ModuleLiteral(_) + | Type::ClassLiteral(_) + | Type::IntLiteral(_) => self, } } @@ -5604,6 +5614,18 @@ impl<'db> TypeMapping<'_, 'db> { TypeMapping::PromoteLiterals => TypeMapping::PromoteLiterals, } } + + fn normalized(&self, db: &'db dyn Db) -> Self { + match self { + TypeMapping::Specialization(specialization) => { + TypeMapping::Specialization(specialization.normalized(db)) + } + TypeMapping::PartialSpecialization(partial) => { + TypeMapping::PartialSpecialization(partial.normalized(db)) + } + TypeMapping::PromoteLiterals => TypeMapping::PromoteLiterals, + } + } } #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] @@ -5627,6 +5649,13 @@ pub enum DynamicType { TodoPEP695ParamSpec, } +impl DynamicType { + #[expect(clippy::unused_self)] + fn normalized(self) -> Self { + Self::Any + } +} + impl std::fmt::Display for DynamicType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -5877,6 +5906,18 @@ impl<'db> TypeVarInstance<'db> { None } } + + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { + Self::new( + db, + self.name(db), + self.definition(db), + self.bound_or_constraints(db).map(|b| b.normalized(db)), + self.variance(db), + self.default_ty(db).map(|d| d.normalized(db)), + self.kind(db), + ) + } } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)] @@ -5893,6 +5934,19 @@ pub enum TypeVarBoundOrConstraints<'db> { Constraints(UnionType<'db>), } +impl<'db> TypeVarBoundOrConstraints<'db> { + fn normalized(self, db: &'db dyn Db) -> Self { + match self { + TypeVarBoundOrConstraints::UpperBound(bound) => { + TypeVarBoundOrConstraints::UpperBound(bound.normalized(db)) + } + TypeVarBoundOrConstraints::Constraints(constraints) => { + TypeVarBoundOrConstraints::Constraints(constraints.normalized(db)) + } + } + } +} + /// Error returned if a type is not (or may not be) a context manager. #[derive(Debug)] enum ContextManagerError<'db> { @@ -7123,6 +7177,29 @@ impl<'db> FunctionType<'db> { .is_gradual_equivalent_to(db, other.into_callable_type(db)) } + fn normalized(self, db: &'db dyn Db) -> Self { + let context = self + .inherited_generic_context(db) + .map(|ctx| ctx.normalized(db)); + + let mappings: Box<_> = self + .type_mappings(db) + .iter() + .map(|mapping| mapping.normalized(db)) + .collect(); + + Self::new( + db, + self.name(db), + self.known(db), + self.body_scope(db), + self.decorators(db), + self.dataclass_transformer_params(db), + context, + mappings, + ) + } + /// Returns a tuple of two spans. The first is /// the span for the identifier of the function /// definition for `self`. The second is @@ -7410,6 +7487,14 @@ impl<'db> BoundMethodType<'db> { )) } + fn normalized(self, db: &'db dyn Db) -> Self { + Self::new( + db, + self.function(db).normalized(db), + self.self_instance(db).normalized(db), + ) + } + fn is_subtype_of(self, db: &'db dyn Db, other: Self) -> bool { // A bound method is a typically a subtype of itself. However, we must explicitly verify // the subtyping of the underlying function signatures (since they might be specialized @@ -7813,6 +7898,24 @@ impl<'db> MethodWrapperKind<'db> { ) => false, } } + + fn normalized(self, db: &'db dyn Db) -> Self { + match self { + MethodWrapperKind::FunctionTypeDunderGet(function) => { + MethodWrapperKind::FunctionTypeDunderGet(function.normalized(db)) + } + MethodWrapperKind::FunctionTypeDunderCall(function) => { + MethodWrapperKind::FunctionTypeDunderCall(function.normalized(db)) + } + MethodWrapperKind::PropertyDunderGet(property) => { + MethodWrapperKind::PropertyDunderGet(property.normalized(db)) + } + MethodWrapperKind::PropertyDunderSet(property) => { + MethodWrapperKind::PropertyDunderSet(property.normalized(db)) + } + MethodWrapperKind::StrStartswith(_) => self, + } + } } /// Represents a specific instance of `types.WrapperDescriptorType` @@ -7907,6 +8010,10 @@ impl<'db> PEP695TypeAliasType<'db> { let definition = self.definition(db); definition_expression_type(db, definition, &type_alias_stmt_node.value) } + + fn normalized(self, _db: &'db dyn Db) -> Self { + self + } } /// # Ordering @@ -7921,6 +8028,17 @@ pub struct BareTypeAliasType<'db> { pub value: Type<'db>, } +impl<'db> BareTypeAliasType<'db> { + fn normalized(self, db: &'db dyn Db) -> Self { + Self::new( + db, + self.name(db), + self.definition(db), + self.value(db).normalized(db), + ) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, salsa::Update)] pub enum TypeAliasType<'db> { PEP695(PEP695TypeAliasType<'db>), @@ -7928,10 +8046,17 @@ pub enum TypeAliasType<'db> { } impl<'db> TypeAliasType<'db> { + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { + match self { + TypeAliasType::PEP695(type_alias) => TypeAliasType::PEP695(type_alias.normalized(db)), + TypeAliasType::Bare(type_alias) => TypeAliasType::Bare(type_alias.normalized(db)), + } + } + pub(crate) fn name(self, db: &'db dyn Db) -> &'db str { match self { TypeAliasType::PEP695(type_alias) => type_alias.name(db), - TypeAliasType::Bare(type_alias) => type_alias.name(db).as_str(), + TypeAliasType::Bare(type_alias) => type_alias.name(db), } } @@ -8595,6 +8720,14 @@ pub enum SuperOwnerKind<'db> { } impl<'db> SuperOwnerKind<'db> { + fn normalized(self, db: &'db dyn Db) -> Self { + match self { + SuperOwnerKind::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic.normalized()), + SuperOwnerKind::Class(class) => SuperOwnerKind::Class(class.normalized(db)), + SuperOwnerKind::Instance(instance) => SuperOwnerKind::Instance(instance.normalized(db)), + } + } + fn iter_mro(self, db: &'db dyn Db) -> impl Iterator> { match self { SuperOwnerKind::Dynamic(dynamic) => { @@ -8829,6 +8962,14 @@ impl<'db> BoundSuperType<'db> { ), } } + + fn normalized(self, db: &'db dyn Db) -> Self { + Self::new( + db, + self.pivot_class(db).normalized(db), + self.owner(db).normalized(db), + ) + } } // Make sure that the `Type` enum does not grow unexpectedly. diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index d8ad6b444c..edb304fec1 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -31,6 +31,19 @@ impl<'db> ClassBase<'db> { Self::Dynamic(DynamicType::Unknown) } + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { + match self { + Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()), + Self::Class(class) => Self::Class(class.normalized(db)), + Self::Protocol(generic_context) => { + Self::Protocol(generic_context.map(|context| context.normalized(db))) + } + Self::Generic(generic_context) => { + Self::Generic(generic_context.map(|context| context.normalized(db))) + } + } + } + pub(crate) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db { struct Display<'db> { base: ClassBase<'db>, diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index eae850bbce..c3d5a61fb7 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -237,6 +237,15 @@ impl<'db> GenericContext<'db> { Specialization::new(db, self, expanded.into_boxed_slice()) } + + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { + let variables: FxOrderSet<_> = self + .variables(db) + .iter() + .map(|ty| ty.normalized(db)) + .collect(); + Self::new(db, variables, self.origin(db)) + } } #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] @@ -561,6 +570,16 @@ impl<'db> PartialSpecialization<'_, 'db> { types: Cow::from(self.types.clone().into_owned()), } } + + pub(crate) fn normalized(&self, db: &'db dyn Db) -> PartialSpecialization<'db, 'db> { + let generic_context = self.generic_context.normalized(db); + let types: Cow<_> = self.types.iter().map(|ty| ty.normalized(db)).collect(); + + PartialSpecialization { + generic_context, + types, + } + } } /// Performs type inference between parameter annotations and argument types, producing a diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs index 3d0fefe85f..a310a98941 100644 --- a/crates/ty_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -153,6 +153,53 @@ impl<'db> KnownInstanceType<'db> { } } + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { + match self { + Self::Annotated + | Self::Literal + | Self::LiteralString + | Self::Optional + | Self::Union + | Self::NoReturn + | Self::Never + | Self::Tuple + | Self::Type + | Self::TypingSelf + | Self::Final + | Self::ClassVar + | Self::Callable + | Self::Concatenate + | Self::Unpack + | Self::Required + | Self::NotRequired + | Self::TypeAlias + | Self::TypeGuard + | Self::TypedDict + | Self::TypeIs + | Self::List + | Self::Dict + | Self::DefaultDict + | Self::Set + | Self::FrozenSet + | Self::Counter + | Self::Deque + | Self::ChainMap + | Self::OrderedDict + | Self::ReadOnly + | Self::Unknown + | Self::AlwaysTruthy + | Self::AlwaysFalsy + | Self::Not + | Self::Intersection + | Self::TypeOf + | Self::CallableTypeOf => self, + Self::TypeVar(tvar) => Self::TypeVar(tvar.normalized(db)), + Self::Protocol(ctx) => Self::Protocol(ctx.map(|ctx| ctx.normalized(db))), + Self::Generic(ctx) => Self::Generic(ctx.map(|ctx| ctx.normalized(db))), + Self::TypeAliasType(alias) => Self::TypeAliasType(alias.normalized(db)), + } + } + /// Return the repr of the symbol at runtime pub(crate) fn repr(self, db: &'db dyn Db) -> impl Display + 'db { KnownInstanceRepr { diff --git a/crates/ty_python_semantic/src/types/subclass_of.rs b/crates/ty_python_semantic/src/types/subclass_of.rs index af54b4504a..b177ea92f6 100644 --- a/crates/ty_python_semantic/src/types/subclass_of.rs +++ b/crates/ty_python_semantic/src/types/subclass_of.rs @@ -122,6 +122,12 @@ impl<'db> SubclassOfType<'db> { } } + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { + Self { + subclass_of: self.subclass_of.normalized(db), + } + } + pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { match self.subclass_of { SubclassOfInner::Class(class) => Type::instance(db, class), @@ -173,6 +179,13 @@ impl<'db> SubclassOfInner<'db> { } } + pub(crate) fn normalized(self, db: &'db dyn Db) -> Self { + match self { + Self::Class(class) => Self::Class(class.normalized(db)), + Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()), + } + } + pub(crate) fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option { match ty { Type::Dynamic(dynamic) => Some(Self::Dynamic(dynamic)),