From 54c88b599dcbc3adf50c587d932077fd356a9129 Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 26 Nov 2025 12:02:48 +0100 Subject: [PATCH] Store binding context --- .../resources/mdtest/implicit_type_aliases.md | 2 - crates/ty_python_semantic/src/types.rs | 8 +- crates/ty_python_semantic/src/types/class.rs | 85 ++++++++++++------- .../src/types/class_base.rs | 2 +- .../src/types/infer/builder.rs | 52 ++++++++---- .../types/infer/builder/type_expression.rs | 68 ++++++++++----- .../ty_python_semantic/src/types/instance.rs | 4 +- crates/ty_python_semantic/src/types/mro.rs | 11 +-- crates/ty_python_semantic/src/types/tuple.rs | 45 ++++++---- 9 files changed, 185 insertions(+), 92 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index c2f02be9d5..896ab87138 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -442,8 +442,6 @@ def _( Generic implicit type aliases can be partially specialized: ```py -U = TypeVar("U") - DictStrTo = MyDict[str, U] reveal_type(DictStrTo) # revealed: diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 3e8ea3f8de..e3812035fe 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -6599,7 +6599,7 @@ impl<'db> Type<'db> { .map(|specialization| { Type::instance( db, - generic_origin.apply_specialization(db, |_| specialization), + generic_origin.apply_specialization(db, |_| specialization, None), ) }) .unwrap_or(instance_ty); @@ -7111,7 +7111,11 @@ impl<'db> Type<'db> { pub(crate) fn dunder_class(self, db: &'db dyn Db) -> Type<'db> { if self.is_typed_dict() { return KnownClass::Dict - .to_specialized_class_type(db, [KnownClass::Str.to_instance(db), Type::object()]) + .to_specialized_class_type( + db, + [KnownClass::Str.to_instance(db), Type::object()], + None, + ) .map(Type::from) // Guard against user-customized typesheds with a broken `dict` class .unwrap_or_else(Type::unknown); diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 151d329d47..7f7a87caa0 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -103,7 +103,7 @@ fn try_mro_cycle_initial<'db>( ) -> Result, MroError<'db>> { Err(MroError::cycle( db, - self_.apply_optional_specialization(db, specialization), + self_.apply_optional_specialization(db, specialization, None), )) } @@ -233,6 +233,8 @@ impl<'db> CodeGeneratorKind<'db> { pub struct GenericAlias<'db> { pub(crate) origin: ClassLiteral<'db>, pub(crate) specialization: Specialization<'db>, + + pub(crate) binding_context: Option>, } pub(super) fn walk_generic_alias<'db, V: super::visitor::TypeVisitor<'db> + ?Sized>( @@ -252,6 +254,7 @@ impl<'db> GenericAlias<'db> { db, self.origin(db), self.specialization(db).normalized_impl(db, visitor), + self.binding_context(db), ) } @@ -277,6 +280,7 @@ impl<'db> GenericAlias<'db> { self.origin(db), self.specialization(db) .apply_type_mapping_impl(db, type_mapping, tcx, visitor), + self.binding_context(db), ) } @@ -1529,6 +1533,7 @@ impl<'db> ClassLiteral<'db> { self, db: &'db dyn Db, f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, + binding_context: Option>, ) -> ClassType<'db> { match self.generic_context(db) { None => ClassType::NonGeneric(self), @@ -1545,7 +1550,7 @@ impl<'db> ClassLiteral<'db> { } } - ClassType::Generic(GenericAlias::new(db, self, specialization)) + ClassType::Generic(GenericAlias::new(db, self, specialization, binding_context)) } } } @@ -1554,48 +1559,63 @@ impl<'db> ClassLiteral<'db> { self, db: &'db dyn Db, specialization: Option>, + binding_context: Option>, ) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - specialization - .unwrap_or_else(|| generic_context.default_specialization(db, self.known(db))) - }) + self.apply_specialization( + db, + |generic_context| { + specialization + .unwrap_or_else(|| generic_context.default_specialization(db, self.known(db))) + }, + binding_context, + ) } pub(crate) fn top_materialization(self, db: &'db dyn Db) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - generic_context - .default_specialization(db, self.known(db)) - .materialize_impl( - db, - MaterializationKind::Top, - &ApplyTypeMappingVisitor::default(), - ) - }) + self.apply_specialization( + db, + |generic_context| { + generic_context + .default_specialization(db, self.known(db)) + .materialize_impl( + db, + MaterializationKind::Top, + &ApplyTypeMappingVisitor::default(), + ) + }, + None, + ) } /// Returns the default specialization of this class. For non-generic classes, the class is /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// applies the default specialization to the class's typevars. pub(crate) fn default_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - generic_context.default_specialization(db, self.known(db)) - }) + self.apply_specialization( + db, + |generic_context| generic_context.default_specialization(db, self.known(db)), + None, + ) } /// Returns the unknown specialization of this class. For non-generic classes, the class is /// returned unchanged. For a non-specialized generic class, we return a generic alias that /// maps each of the class's typevars to `Unknown`. pub(crate) fn unknown_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - generic_context.unknown_specialization(db) - }) + self.apply_specialization( + db, + |generic_context| generic_context.unknown_specialization(db), + None, + ) } /// Returns a specialization of this class where each typevar is mapped to itself. pub(crate) fn identity_specialization(self, db: &'db dyn Db) -> ClassType<'db> { - self.apply_specialization(db, |generic_context| { - generic_context.identity_specialization(db) - }) + self.apply_specialization( + db, + |generic_context| generic_context.identity_specialization(db), + None, + ) } /// Return an iterator over the inferred types of this class's *explicit* bases. @@ -1626,7 +1646,7 @@ impl<'db> ClassLiteral<'db> { Box::new([ definition_expression_type(db, class_definition, &class_stmt.bases()[0]), - Type::from(tuple_type.to_class_type(db)), + Type::from(tuple_type.to_class_type(db, None)), ]) } else { class_stmt @@ -2271,8 +2291,10 @@ impl<'db> ClassLiteral<'db> { || transformer_params.is_some_and(|params| params.flags(db).contains(param)) }; - let instance_ty = - Type::instance(db, self.apply_optional_specialization(db, specialization)); + let instance_ty = Type::instance( + db, + self.apply_optional_specialization(db, specialization, None), + ); let signature_from_fields = |mut parameters: Vec<_>, return_ty: Option>| { for (field_name, field) in self.fields(db, specialization, field_policy) { @@ -4787,6 +4809,7 @@ impl KnownClass { self, db: &'db dyn Db, specialization: impl IntoIterator>, + binding_context: Option>, ) -> Option> { let Type::ClassLiteral(class_literal) = self.to_class_literal(db) else { return None; @@ -4808,7 +4831,11 @@ impl KnownClass { return Some(class_literal.default_specialization(db)); } - Some(class_literal.apply_specialization(db, |_| generic_context.specialize(db, types))) + Some(class_literal.apply_specialization( + db, + |_| generic_context.specialize(db, types), + binding_context, + )) } /// Lookup a [`KnownClass`] in typeshed and return a [`Type`] @@ -4827,7 +4854,7 @@ impl KnownClass { KnownClass::Tuple, "Use `Type::heterogeneous_tuple` or `Type::homogeneous_tuple` to create `tuple` instances" ); - self.to_specialized_class_type(db, specialization) + self.to_specialized_class_type(db, specialization, None) .and_then(|class_type| Type::from(class_type).to_instance(db)) .unwrap_or_else(Type::unknown) } diff --git a/crates/ty_python_semantic/src/types/class_base.rs b/crates/ty_python_semantic/src/types/class_base.rs index 6d0af17427..e392df287f 100644 --- a/crates/ty_python_semantic/src/types/class_base.rs +++ b/crates/ty_python_semantic/src/types/class_base.rs @@ -239,7 +239,7 @@ impl<'db> ClassBase<'db> { db, fields.values().map(|field| field.declared_ty), )? - .to_class_type(db) + .to_class_type(db, None) .into(), subclass, ) diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index d7ffc9960f..c36a797ae8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -4740,8 +4740,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } TargetKind::Single => { // This could be an implicit type alias (OptionalList = list[T] | None). Use the definition - // of `OptionalList` as the typevar binding context while inferring the RHS (`list[T] | None`), - // in order to bind `T@OptionalList`. + // of `OptionalList` as the binding context while inferring the RHS (`list[T] | None`), in + // order to bind `T` to `OptionalList`. let previous_typevar_binding_context = self.typevar_binding_context.replace(definition); @@ -5531,8 +5531,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.deferred_state = DeferredExpressionState::Deferred; } - let previous_typevar_binding_context = self.typevar_binding_context; - self.typevar_binding_context = Some(definition); + // This might be a PEP-613 type alias (`OptionalList: TypeAlias = list[T] | None`). Use + // the definition of `OptionalList` as the binding context while inferring the + // RHS (`list[T] | None`), in order to bind `T` to `OptionalList`. + let previous_typevar_binding_context = self.typevar_binding_context.replace(definition); let inferred_ty = self.infer_maybe_standalone_expression( value, @@ -7498,7 +7500,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } let class_type = - class_literal.apply_specialization(self.db(), |_| builder.build(generic_context)); + class_literal.apply_specialization(self.db(), |_| builder.build(generic_context), None); Type::from(class_type).to_instance(self.db()) } @@ -10808,13 +10810,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { fn infer_subscript_load(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> { let value_ty = self.infer_expression(&subscript.value, TypeContext::default()); + // If we have an implicit type alias like `MyList = list[T]`, and if `MyList` is being + // used in another implicit type alias like `Numbers = MyList[int]`, then we infer the + // right hand side as a value expression, and need to handle the specialization here. if let Some(alias) = value_ty.as_generic_alias() { - return self.infer_explicitly_specialized_type_alias( + let return_ty = self.infer_explicitly_specialized_type_alias( subscript, value_ty, - Some(alias.definition(self.db())), + alias.binding_context(self.db()), false, ); + + return return_ty; } self.infer_subscript_load_impl(value_ty, subscript) @@ -10853,9 +10860,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - let tuple_generic_alias = |db: &'db dyn Db, tuple: Option>| { + let db = self.db(); + let typevar_binding_context = self.typevar_binding_context; + let tuple_generic_alias = |tuple: Option>| { let tuple = tuple.unwrap_or_else(|| TupleType::homogeneous(db, Type::unknown())); - Type::from(tuple.to_class_type(db)) + Type::from(tuple.to_class_type(db, typevar_binding_context)) }; match value_ty { @@ -10867,7 +10876,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // updating all of the subscript logic below to use custom callables for all of the _other_ // special cases, too. if class.is_tuple(self.db()) { - return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice)); + return tuple_generic_alias(self.infer_tuple_type_expression(slice)); } else if class.is_known(self.db(), KnownClass::Type) { let argument_ty = self.infer_type_expression(slice); return Type::KnownInstance(KnownInstanceType::TypeGenericAlias( @@ -10895,7 +10904,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } Type::SpecialForm(SpecialFormType::Tuple) => { - return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice)); + return tuple_generic_alias(self.infer_tuple_type_expression(slice)); } Type::SpecialForm(SpecialFormType::Literal) => { match self.infer_literal_parameter_type(slice) { @@ -11061,7 +11070,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .expect("A known stdlib class is available"); return class - .to_specialized_class_type(self.db(), [element_ty]) + .to_specialized_class_type( + self.db(), + [element_ty], + self.typevar_binding_context, + ) .map(Type::from) .unwrap_or_else(Type::unknown); } @@ -11119,7 +11132,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .expect("Stdlib class available"); return class - .to_specialized_class_type(self.db(), [first_ty, second_ty]) + .to_specialized_class_type( + self.db(), + [first_ty, second_ty], + self.typevar_binding_context, + ) .map(Type::from) .unwrap_or_else(Type::unknown); } @@ -11166,10 +11183,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { generic_context: GenericContext<'db>, ) -> Type<'db> { let db = self.db(); + let typevar_binding_context = self.typevar_binding_context; let specialize = |types: &[Option>]| { - Type::from(generic_class.apply_specialization(db, |_| { - generic_context.specialize_partial(db, types.iter().copied()) - })) + Type::from(generic_class.apply_specialization( + db, + |_| generic_context.specialize_partial(db, types.iter().copied()), + typevar_binding_context, + )) }; self.infer_explicit_callable_specialization( diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 3256e13dc7..8801217759 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -13,9 +13,10 @@ use crate::types::string_annotation::parse_string_annotation; use crate::types::tuple::{TupleSpecBuilder, TupleType}; use crate::types::visitor::any_over_type; use crate::types::{ - BindingContext, CallableType, DynamicType, GenericContext, IntersectionBuilder, KnownClass, - KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, SubclassOfType, - Type, TypeAliasType, TypeContext, TypeIsType, TypeMapping, UnionBuilder, UnionType, todo_type, + BindingContext, CallableType, DynamicType, GenericAlias, GenericContext, IntersectionBuilder, + KnownClass, KnownInstanceType, LintDiagnosticGuard, Parameter, Parameters, SpecialFormType, + SubclassOfType, Type, TypeAliasType, TypeContext, TypeInContext, TypeIsType, TypeMapping, + UnionBuilder, UnionType, todo_type, }; /// Type expressions @@ -712,13 +713,20 @@ impl<'db> TypeInferenceBuilder<'db, '_> { match class_literal.generic_context(self.db()) { Some(generic_context) => { let db = self.db(); + let typevar_binding_context = self.typevar_binding_context; let specialize = |types: &[Option>]| { SubclassOfType::from( db, - class_literal.apply_specialization(db, |_| { - generic_context - .specialize_partial(db, types.iter().copied()) - }), + class_literal.apply_specialization( + db, + |_| { + generic_context.specialize_partial( + db, + types.iter().copied(), + ) + }, + typevar_binding_context, + ), ) }; self.infer_explicit_callable_specialization( @@ -756,7 +764,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { pub(crate) fn infer_explicitly_specialized_type_alias( &mut self, subscript: &ast::ExprSubscript, - value_ty: Type<'db>, + mut value_ty: Type<'db>, typevar_binding_context: Option>, in_type_expression: bool, ) -> Type<'db> { @@ -773,30 +781,52 @@ impl<'db> TypeInferenceBuilder<'db, '_> { return Type::unknown(); }; - let generic_type_alias = value_ty.apply_type_mapping( - db, - &TypeMapping::BindLegacyTypevars(BindingContext::Definition(typevar_binding_context)), - TypeContext::default(), - ); + if let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) = value_ty + && let Some(definition) = typevar.definition(db) + { + value_ty = value_ty.apply_type_mapping( + db, + &TypeMapping::BindLegacyTypevars(BindingContext::Definition(definition)), + TypeContext::default(), + ); + } let mut variables = FxOrderSet::default(); - generic_type_alias.find_legacy_typevars(db, None, &mut variables); + value_ty.find_legacy_typevars(db, Some(typevar_binding_context), &mut variables); let generic_context = GenericContext::from_typevar_instances(db, variables); let scope_id = self.scope(); - let typevar_binding_context = self.typevar_binding_context; + let current_typevar_binding_context = self.typevar_binding_context; let specialize = |types: &[Option>]| { - let specialized = generic_type_alias.apply_specialization( + let specialized = value_ty.apply_specialization( db, generic_context.specialize_partial(db, types.iter().copied()), ); if in_type_expression { specialized - .in_type_expression(db, scope_id, typevar_binding_context) + .in_type_expression(db, scope_id, current_typevar_binding_context) .unwrap_or_else(|_| Type::unknown()) } else { - specialized + // Update the binding context + match specialized { + Type::GenericAlias(alias) => Type::GenericAlias(GenericAlias::new( + db, + alias.origin(db), + alias.specialization(db), + current_typevar_binding_context, + )), + Type::KnownInstance(KnownInstanceType::TypeGenericAlias(instance)) => { + Type::KnownInstance(KnownInstanceType::TypeGenericAlias( + TypeInContext::new( + db, + instance.inner(db), + current_typevar_binding_context, + ), + )) + } + _ => specialized, + } } }; @@ -1022,7 +1052,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { Type::GenericAlias(alias) => self.infer_explicitly_specialized_type_alias( subscript, value_ty, - Some(alias.definition(self.db())), + alias.binding_context(self.db()), true, ), Type::StringLiteral(_) => { diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 04a560be83..a0218e4276 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -213,7 +213,7 @@ pub(super) fn walk_nominal_instance_type<'db, V: super::visitor::TypeVisitor<'db impl<'db> NominalInstanceType<'db> { pub(super) fn class(&self, db: &'db dyn Db) -> ClassType<'db> { match self.0 { - NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db), + NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db, None), NominalInstanceInner::NonTuple(class) => class, NominalInstanceInner::Object => KnownClass::Object .try_to_class_literal(db) @@ -224,7 +224,7 @@ impl<'db> NominalInstanceType<'db> { pub(super) fn class_literal(&self, db: &'db dyn Db) -> ClassLiteral<'db> { let class = match self.0 { - NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db), + NominalInstanceInner::ExactTuple(tuple) => tuple.to_class_type(db, None), NominalInstanceInner::NonTuple(class) => class, NominalInstanceInner::Object => { return KnownClass::Object diff --git a/crates/ty_python_semantic/src/types/mro.rs b/crates/ty_python_semantic/src/types/mro.rs index 21501060da..34bd1a58d8 100644 --- a/crates/ty_python_semantic/src/types/mro.rs +++ b/crates/ty_python_semantic/src/types/mro.rs @@ -51,7 +51,7 @@ impl<'db> Mro<'db> { class_literal: ClassLiteral<'db>, specialization: Option>, ) -> Result> { - let class = class_literal.apply_optional_specialization(db, specialization); + let class = class_literal.apply_optional_specialization(db, specialization, None); // Special-case `NotImplementedType`: typeshed says that it inherits from `Any`, // but this causes more problems than it fixes. if class_literal.is_known(db, KnownClass::NotImplementedType) { @@ -412,10 +412,11 @@ impl<'db> Iterator for MroIterator<'db> { fn next(&mut self) -> Option { if !self.first_element_yielded { self.first_element_yielded = true; - return Some(ClassBase::Class( - self.class - .apply_optional_specialization(self.db, self.specialization), - )); + return Some(ClassBase::Class(self.class.apply_optional_specialization( + self.db, + self.specialization, + None, + ))); } self.full_mro_except_first_element().next() } diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index e773315d68..4158ce629c 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -202,19 +202,27 @@ impl<'db> TupleType<'db> { // `static-frame` as part of a mypy_primer run! This is because it's called // from `NominalInstanceType::class()`, which is a very hot method. #[salsa::tracked(cycle_initial=to_class_type_cycle_initial, heap_size=ruff_memory_usage::heap_size)] - pub(crate) fn to_class_type(self, db: &'db dyn Db) -> ClassType<'db> { + pub(crate) fn to_class_type( + self, + db: &'db dyn Db, + binding_context: Option>, + ) -> ClassType<'db> { let tuple_class = KnownClass::Tuple .try_to_class_literal(db) .expect("Typeshed should always have a `tuple` class in `builtins.pyi`"); - tuple_class.apply_specialization(db, |generic_context| { - if generic_context.variables(db).len() == 1 { - let element_type = self.tuple(db).homogeneous_element_type(db); - generic_context.specialize_tuple(db, element_type, self) - } else { - generic_context.default_specialization(db, Some(KnownClass::Tuple)) - } - }) + tuple_class.apply_specialization( + db, + |generic_context| { + if generic_context.variables(db).len() == 1 { + let element_type = self.tuple(db).homogeneous_element_type(db); + generic_context.specialize_tuple(db, element_type, self) + } else { + generic_context.default_specialization(db, Some(KnownClass::Tuple)) + } + }, + binding_context, + ) } /// Return a normalized version of `self`. @@ -294,18 +302,23 @@ fn to_class_type_cycle_initial<'db>( db: &'db dyn Db, _id: salsa::Id, self_: TupleType<'db>, + binding_context: Option>, ) -> ClassType<'db> { let tuple_class = KnownClass::Tuple .try_to_class_literal(db) .expect("Typeshed should always have a `tuple` class in `builtins.pyi`"); - tuple_class.apply_specialization(db, |generic_context| { - if generic_context.variables(db).len() == 1 { - generic_context.specialize_tuple(db, Type::Never, self_) - } else { - generic_context.default_specialization(db, Some(KnownClass::Tuple)) - } - }) + tuple_class.apply_specialization( + db, + |generic_context| { + if generic_context.variables(db).len() == 1 { + generic_context.specialize_tuple(db, Type::Never, self_) + } else { + generic_context.default_specialization(db, Some(KnownClass::Tuple)) + } + }, + binding_context, + ) } /// A tuple spec describes the contents of a tuple type, which might be fixed- or variable-length.