diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 0867983e3a..45b82ca8b0 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -312,14 +312,14 @@ mod tests { This is such a great class!! Don't you know? - + Everyone loves my class!! ''' def __init__(self, val): """initializes MyClass (perfectly)""" self.val = val - + def my_method(self, a, b): '''This is such a great func!! @@ -379,14 +379,14 @@ mod tests { This is such a great class!! Don't you know? - + Everyone loves my class!! ''' def __init__(self, val): """initializes MyClass (perfectly)""" self.val = val - + def my_method(self, a, b): '''This is such a great func!! @@ -444,14 +444,14 @@ mod tests { This is such a great class!! Don't you know? - + Everyone loves my class!! ''' def __init__(self, val): """initializes MyClass (perfectly)""" self.val = val - + def my_method(self, a, b): '''This is such a great func!! @@ -505,7 +505,7 @@ mod tests { This is such a great class!! Don't you know? - + Everyone loves my class!! ''' @@ -562,13 +562,13 @@ mod tests { This is such a great class!! Don't you know? - + Everyone loves my class!! ''' def __init__(self, val): self.val = val - + def my_method(self, a, b): '''This is such a great func!! @@ -628,14 +628,14 @@ mod tests { This is such a great class!! Don't you know? - + Everyone loves my class!! ''' def __init__(self, val): """initializes MyClass (perfectly)""" self.val = val - + def my_method(self, a, b): '''This is such a great func!! @@ -1589,12 +1589,11 @@ def ab(a: int, *, c: int): "#, ); - // TODO: This should render T@Alias once we create GenericContexts for type alias scopes. assert_snapshot!(test.hover(), @r" - typing.TypeVar + T@Alias --------------------------------------------- ```python - typing.TypeVar + T@Alias ``` --------------------------------------------- info[hover]: Hovered content is @@ -1875,9 +1874,9 @@ def ab(a: int, *, c: int): def foo(a: str | None, b): ''' My cool func - + Args: - a: hopefully a string, right?! + a: hopefully a string, right?! ''' if a is not None: print(a) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md new file mode 100644 index 0000000000..4ea9e7adf8 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md @@ -0,0 +1,172 @@ +# Generic type aliases: PEP 695 syntax + +```toml +[environment] +python-version = "3.13" +``` + +## Defining a generic alias + +At its simplest, to define a type alias using PEP 695 syntax, you add a list of `TypeVar`s, +`ParamSpec`s or `TypeVarTuple`s after the alias name. + +```py +from ty_extensions import generic_context + +type SingleTypevar[T] = ... +type MultipleTypevars[T, S] = ... +type SingleParamSpec[**P] = ... +type TypeVarAndParamSpec[T, **P] = ... +type SingleTypeVarTuple[*Ts] = ... +type TypeVarAndTypeVarTuple[T, *Ts] = ... + +# revealed: tuple[T@SingleTypevar] +reveal_type(generic_context(SingleTypevar)) +# revealed: tuple[T@MultipleTypevars, S@MultipleTypevars] +reveal_type(generic_context(MultipleTypevars)) + +# TODO: support `ParamSpec`/`TypeVarTuple` properly +# (these should include the `ParamSpec`s and `TypeVarTuple`s in their generic contexts) +reveal_type(generic_context(SingleParamSpec)) # revealed: tuple[()] +reveal_type(generic_context(TypeVarAndParamSpec)) # revealed: tuple[T@TypeVarAndParamSpec] +reveal_type(generic_context(SingleTypeVarTuple)) # revealed: tuple[()] +reveal_type(generic_context(TypeVarAndTypeVarTuple)) # revealed: tuple[T@TypeVarAndTypeVarTuple] +``` + +You cannot use the same typevar more than once. + +```py +# error: [invalid-syntax] "duplicate type parameter" +type RepeatedTypevar[T, T] = ... +``` + +## Specializing type aliases explicitly + +The type parameter can be specified explicitly: + +```py +from typing import Literal + +type C[T] = T + +def _(a: C[int], b: C[Literal[5]]): + reveal_type(a) # revealed: int + reveal_type(b) # revealed: Literal[5] +``` + +The specialization must match the generic types: + +```py +# error: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2" +reveal_type(C[int, int]) # revealed: Unknown +``` + +And non-generic types cannot be specialized: + +```py +type B = ... + +# error: [non-subscriptable] "Cannot subscript non-generic type alias" +reveal_type(B[int]) # revealed: Unknown + +# error: [non-subscriptable] "Cannot subscript non-generic type alias" +def _(b: B[int]): ... +``` + +If the type variable has an upper bound, the specialized type must satisfy that bound: + +```py +type Bounded[T: int] = ... +type BoundedByUnion[T: int | str] = ... + +class IntSubclass(int): ... + +reveal_type(Bounded[int]) # revealed: Bounded[int] +reveal_type(Bounded[IntSubclass]) # revealed: Bounded[IntSubclass] + +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `str`" +reveal_type(Bounded[str]) # revealed: Unknown + +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `int | str`" +reveal_type(Bounded[int | str]) # revealed: Unknown + +reveal_type(BoundedByUnion[int]) # revealed: BoundedByUnion[int] +reveal_type(BoundedByUnion[IntSubclass]) # revealed: BoundedByUnion[IntSubclass] +reveal_type(BoundedByUnion[str]) # revealed: BoundedByUnion[str] +reveal_type(BoundedByUnion[int | str]) # revealed: BoundedByUnion[int | str] +``` + +If the type variable is constrained, the specialized type must satisfy those constraints: + +```py +type Constrained[T: (int, str)] = ... + +reveal_type(Constrained[int]) # revealed: Constrained[int] + +# TODO: error: [invalid-argument-type] +# TODO: revealed: Constrained[Unknown] +reveal_type(Constrained[IntSubclass]) # revealed: Constrained[IntSubclass] + +reveal_type(Constrained[str]) # revealed: Constrained[str] + +# TODO: error: [invalid-argument-type] +# TODO: revealed: Unknown +reveal_type(Constrained[int | str]) # revealed: Constrained[int | str] + +# TODO: update this diagnostic to talk about type parameters and specializations +# error: [invalid-argument-type] "Argument is incorrect: Expected `int | str`, found `object`" +reveal_type(Constrained[object]) # revealed: Unknown +``` + +If the type variable has a default, it can be omitted: + +```py +type WithDefault[T, U = int] = ... + +reveal_type(WithDefault[str, str]) # revealed: WithDefault[str, str] +reveal_type(WithDefault[str]) # revealed: WithDefault[str, int] +``` + +If the type alias is not specialized explicitly, it is implicitly specialized to `Unknown`: + +```py +type G[T] = list[T] + +def _(g: G): + reveal_type(g) # revealed: list[Unknown] +``` + +Unless a type default was provided: + +```py +type G[T = int] = list[T] + +def _(g: G): + reveal_type(g) # revealed: list[int] +``` + +## Aliases are not callable + +```py +type A = int +type B[T] = T + +# error: [call-non-callable] "Object of type `TypeAliasType` is not callable" +reveal_type(A()) # revealed: Unknown + +# error: [call-non-callable] "Object of type `GenericAlias` is not callable" +reveal_type(B[int]()) # revealed: Unknown +``` + +## Recursive Truthiness + +Make sure we handle cycles correctly when computing the truthiness of a generic type alias: + +```py +type X[T: X] = T + +def _(x: X): + assert x +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 467b6ed42c..73607224c9 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -40,13 +40,6 @@ You cannot use the same typevar more than once. class RepeatedTypevar[T, T]: ... ``` -You can only use typevars (TODO: or param specs or typevar tuples) in the class's generic context. - -```py -# TODO: error -class GenericOfType[int]: ... -``` - You can also define a generic class by inheriting from some _other_ generic class, and specializing it with typevars. With PEP 695 syntax, you must explicitly list all of the typevars that you use in your base classes. 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 7599398632..966f2176de 100644 --- a/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/pep695_type_aliases.md @@ -188,7 +188,7 @@ T = TypeVar("T") IntAnd = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,)) def f(x: IntAnd[str]) -> None: - reveal_type(x) # revealed: @Todo(Generic PEP-695 type alias) + reveal_type(x) # revealed: @Todo(Generic manual PEP-695 type alias) ``` ### Error cases diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index f8e8177606..b915a0ef94 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -44,6 +44,7 @@ use crate::types::constraints::{ use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; pub use crate::types::display::DisplaySettings; +use crate::types::display::TupleSpecialization; use crate::types::enums::{enum_metadata, is_single_member_enum}; use crate::types::function::{ DataclassTransformerParams, FunctionSpans, FunctionType, KnownFunction, @@ -196,6 +197,11 @@ pub(crate) struct IsEquivalent; pub(crate) type FindLegacyTypeVarsVisitor<'db> = CycleDetector, ()>; pub(crate) struct FindLegacyTypeVars; +/// A [`CycleDetector`] that is used in `try_bool` methods. +pub(crate) type TryBoolVisitor<'db> = + CycleDetector, Result>>; +pub(crate) struct TryBool; + /// A [`TypeTransformer`] that is used in `normalized` methods. pub(crate) type NormalizedVisitor<'db> = TypeTransformer<'db, Normalized>; pub(crate) struct Normalized; @@ -844,6 +850,13 @@ impl<'db> Type<'db> { } } + pub(crate) const fn into_type_alias(self) -> Option> { + match self { + Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => Some(type_alias), + _ => None, + } + } + pub(crate) const fn into_dynamic(self) -> Option { match self { Type::Dynamic(dynamic_type) => Some(dynamic_type), @@ -3607,7 +3620,7 @@ impl<'db> Type<'db> { /// is truthy or falsy in a context where Python doesn't make an implicit `bool` call. /// Use [`try_bool`](Self::try_bool) for type checking or implicit `bool` calls. pub(crate) fn bool(&self, db: &'db dyn Db) -> Truthiness { - self.try_bool_impl(db, true) + self.try_bool_impl(db, true, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous))) .unwrap_or_else(|err| err.fallback_truthiness()) } @@ -3618,7 +3631,7 @@ impl<'db> Type<'db> { /// /// Returns an error if the type doesn't implement `__bool__` correctly. pub(crate) fn try_bool(&self, db: &'db dyn Db) -> Result> { - self.try_bool_impl(db, false) + self.try_bool_impl(db, false, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous))) } /// Resolves the boolean value of a type. @@ -3637,6 +3650,7 @@ impl<'db> Type<'db> { &self, db: &'db dyn Db, allow_short_circuit: bool, + visitor: &TryBoolVisitor<'db>, ) -> Result> { let type_to_truthiness = |ty| { if let Type::BooleanLiteral(bool_val) = ty { @@ -3706,14 +3720,15 @@ impl<'db> Type<'db> { let mut has_errors = false; for element in union.elements(db) { - let element_truthiness = match element.try_bool_impl(db, allow_short_circuit) { - Ok(truthiness) => truthiness, - Err(err) => { - has_errors = true; - all_not_callable &= matches!(err, BoolError::NotCallable { .. }); - err.fallback_truthiness() - } - }; + let element_truthiness = + match element.try_bool_impl(db, allow_short_circuit, visitor) { + Ok(truthiness) => truthiness, + Err(err) => { + has_errors = true; + all_not_callable &= matches!(err, BoolError::NotCallable { .. }); + err.fallback_truthiness() + } + }; truthiness.get_or_insert(element_truthiness); @@ -3768,17 +3783,19 @@ impl<'db> Type<'db> { Type::AlwaysFalsy => Truthiness::AlwaysFalse, - Type::ClassLiteral(class) => class - .metaclass_instance_type(db) - .try_bool_impl(db, allow_short_circuit)?, + Type::ClassLiteral(class) => { + class + .metaclass_instance_type(db) + .try_bool_impl(db, allow_short_circuit, visitor)? + } Type::GenericAlias(alias) => ClassType::from(*alias) .metaclass_instance_type(db) - .try_bool_impl(db, allow_short_circuit)?, + .try_bool_impl(db, allow_short_circuit, visitor)?, Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { SubclassOfInner::Dynamic(_) => Truthiness::Ambiguous, SubclassOfInner::Class(class) => { - Type::from(class).try_bool_impl(db, allow_short_circuit)? + Type::from(class).try_bool_impl(db, allow_short_circuit, visitor)? } }, @@ -3786,7 +3803,7 @@ impl<'db> Type<'db> { match bound_typevar.typevar(db).bound_or_constraints(db) { None => Truthiness::Ambiguous, Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { - bound.try_bool_impl(db, allow_short_circuit)? + bound.try_bool_impl(db, allow_short_circuit, visitor)? } Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { try_union(constraints)? @@ -3821,9 +3838,11 @@ impl<'db> Type<'db> { Type::BooleanLiteral(bool) => Truthiness::from(*bool), Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()), Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()), - Type::TypeAlias(alias) => alias - .value_type(db) - .try_bool_impl(db, allow_short_circuit)?, + Type::TypeAlias(alias) => visitor.visit(*self, || { + alias + .value_type(db) + .try_bool_impl(db, allow_short_circuit, visitor) + })?, }; Ok(truthiness) @@ -6846,10 +6865,13 @@ impl<'db> KnownInstanceType<'db> { } } - const fn class(self) -> KnownClass { + fn class(self, db: &'db dyn Db) -> KnownClass { match self { Self::SubscriptedProtocol(_) | Self::SubscriptedGeneric(_) => KnownClass::SpecialForm, Self::TypeVar(_) => KnownClass::TypeVar, + Self::TypeAliasType(TypeAliasType::PEP695(alias)) if alias.is_specialized(db) => { + KnownClass::GenericAlias + } Self::TypeAliasType(_) => KnownClass::TypeAliasType, Self::Deprecated(_) => KnownClass::Deprecated, Self::Field(_) => KnownClass::Field, @@ -6857,7 +6879,7 @@ impl<'db> KnownInstanceType<'db> { } fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { - self.class().to_class_literal(db) + self.class(db).to_class_literal(db) } /// Return the instance type which this type is a subtype of. @@ -6866,12 +6888,12 @@ impl<'db> KnownInstanceType<'db> { /// `typing.TypeAliasType`, so `KnownInstanceType::TypeAliasType(_).instance_fallback(db)` /// returns `Type::NominalInstance(NominalInstanceType { class: })`. fn instance_fallback(self, db: &dyn Db) -> Type<'_> { - self.class().to_instance(db) + self.class(db).to_instance(db) } /// Return `true` if this symbol is an instance of `class`. fn is_instance_of(self, db: &dyn Db, class: ClassType) -> bool { - self.class().is_subclass_of(db, class) + self.class(db).is_subclass_of(db, class) } /// Return the repr of the symbol at runtime @@ -6892,7 +6914,16 @@ impl<'db> KnownInstanceType<'db> { f.write_str("typing.Generic")?; generic_context.display(self.db).fmt(f) } - KnownInstanceType::TypeAliasType(_) => f.write_str("typing.TypeAliasType"), + KnownInstanceType::TypeAliasType(alias) => { + if let Some(specialization) = alias.specialization(self.db) { + f.write_str(alias.name(self.db))?; + specialization + .display_short(self.db, TupleSpecialization::No) + .fmt(f) + } else { + f.write_str("typing.TypeAliasType") + } + } // This is a legacy `TypeVar` _outside_ of any generic class or function, so we render // it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll // have a `Type::TypeVar(_)`, which is rendered as the typevar's name. @@ -7507,7 +7538,7 @@ impl<'db> TypeVarInstance<'db> { )) } - #[salsa::tracked] + #[salsa::tracked(cycle_fn=lazy_bound_cycle_recover, cycle_initial=lazy_bound_cycle_initial)] fn lazy_bound(self, db: &'db dyn Db) -> Option> { let definition = self.definition(db)?; let module = parsed_module(db, definition.file(db)).load(db); @@ -7539,6 +7570,23 @@ impl<'db> TypeVarInstance<'db> { } } +#[allow(clippy::ref_option)] +fn lazy_bound_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &Option>, + _count: u32, + _self: TypeVarInstance<'db>, +) -> salsa::CycleRecoveryAction>> { + salsa::CycleRecoveryAction::Iterate +} + +fn lazy_bound_cycle_initial<'db>( + _db: &'db dyn Db, + _self: TypeVarInstance<'db>, +) -> Option> { + None +} + /// Where a type variable is bound and usable. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] pub enum BindingContext<'db> { @@ -9365,6 +9413,8 @@ pub struct PEP695TypeAliasType<'db> { pub name: ast::name::Name, rhs_scope: ScopeId<'db>, + + specialization: Option>, } // The Salsa heap is tracked separately. @@ -9392,7 +9442,65 @@ impl<'db> PEP695TypeAliasType<'db> { let module = parsed_module(db, scope.file(db)).load(db); let type_alias_stmt_node = scope.node(db).expect_type_alias(); let definition = self.definition(db); - definition_expression_type(db, definition, &type_alias_stmt_node.node(&module).value) + let value_type = + definition_expression_type(db, definition, &type_alias_stmt_node.node(&module).value); + + if let Some(generic_context) = self.generic_context(db) { + let specialization = self + .specialization(db) + .unwrap_or_else(|| generic_context.default_specialization(db, None)); + + value_type.apply_specialization(db, specialization) + } else { + value_type + } + } + + pub(crate) fn apply_specialization( + self, + db: &'db dyn Db, + f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, + ) -> PEP695TypeAliasType<'db> { + match self.generic_context(db) { + None => self, + + Some(generic_context) => { + // Note that at runtime, a specialized type alias is an instance of `typing.GenericAlias`. + // However, the `GenericAlias` type in ty is heavily special cased to refer to specialized + // class literals, so we instead represent specialized type aliases as instances of + // `typing.TypeAliasType` internally, and pass the specialization through to the value type, + // except when resolving to an instance of the type alias, or its display representation. + let specialization = f(generic_context); + PEP695TypeAliasType::new( + db, + self.name(db), + self.rhs_scope(db), + Some(specialization), + ) + } + } + } + + pub(crate) fn is_specialized(self, db: &'db dyn Db) -> bool { + self.specialization(db).is_some() + } + + #[salsa::tracked(cycle_fn=generic_context_cycle_recover, cycle_initial=generic_context_cycle_initial, heap_size=ruff_memory_usage::heap_size)] + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + let scope = self.rhs_scope(db); + let file = scope.file(db); + let parsed = parsed_module(db, file).load(db); + let type_alias_stmt_node = scope.node(db).expect_type_alias(); + + type_alias_stmt_node + .node(&parsed) + .type_params + .as_ref() + .map(|type_params| { + let index = semantic_index(db, scope.file(db)); + let definition = index.expect_single_definition(type_alias_stmt_node); + GenericContext::from_type_params(db, index, definition, type_params) + }) } fn normalized_impl(self, _db: &'db dyn Db, _visitor: &NormalizedVisitor<'db>) -> Self { @@ -9400,6 +9508,23 @@ impl<'db> PEP695TypeAliasType<'db> { } } +#[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)] +fn generic_context_cycle_recover<'db>( + _db: &'db dyn Db, + _value: &Option>, + _count: u32, + _self: PEP695TypeAliasType<'db>, +) -> salsa::CycleRecoveryAction>> { + salsa::CycleRecoveryAction::Iterate +} + +fn generic_context_cycle_initial<'db>( + _db: &'db dyn Db, + _self: PEP695TypeAliasType<'db>, +) -> Option> { + None +} + fn value_type_cycle_recover<'db>( _db: &'db dyn Db, _value: &Type<'db>, @@ -9506,6 +9631,41 @@ impl<'db> TypeAliasType<'db> { TypeAliasType::ManualPEP695(type_alias) => type_alias.value(db), } } + + pub(crate) fn into_pep_695_type_alias(self) -> Option> { + match self { + TypeAliasType::PEP695(type_alias) => Some(type_alias), + TypeAliasType::ManualPEP695(_) => None, + } + } + + pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option> { + // TODO: Add support for generic non-PEP695 type aliases. + match self { + TypeAliasType::PEP695(type_alias) => type_alias.generic_context(db), + TypeAliasType::ManualPEP695(_) => None, + } + } + + pub(crate) fn specialization(self, db: &'db dyn Db) -> Option> { + match self { + TypeAliasType::PEP695(type_alias) => type_alias.specialization(db), + TypeAliasType::ManualPEP695(_) => None, + } + } + + pub(crate) fn apply_specialization( + self, + db: &'db dyn Db, + f: impl FnOnce(GenericContext<'db>) -> Specialization<'db>, + ) -> Self { + match self { + TypeAliasType::PEP695(type_alias) => { + TypeAliasType::PEP695(type_alias.apply_specialization(db, f)) + } + TypeAliasType::ManualPEP695(_) => self, + } + } } /// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes. diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 17bec1416b..e08c50b5b8 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -32,8 +32,8 @@ use crate::types::signatures::{Parameter, ParameterForm, Parameters}; use crate::types::tuple::{Tuple, TupleLength, TupleType}; use crate::types::{ BoundMethodType, ClassLiteral, DataclassParams, FieldInstance, KnownClass, KnownInstanceType, - MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType, - WrapperDescriptorKind, enums, ide_support, todo_type, + MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeAliasType, TypeMapping, + UnionType, WrapperDescriptorKind, enums, ide_support, todo_type, }; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity}; use ruff_python_ast::{self as ast, PythonVersion}; @@ -662,6 +662,13 @@ impl<'db> Bindings<'db> { function_generic_context(bound_method.function(db)) } + Type::KnownInstance(KnownInstanceType::TypeAliasType( + TypeAliasType::PEP695(alias), + )) => alias + .generic_context(db) + .map(|generic_context| generic_context.as_tuple(db)) + .unwrap_or_else(|| Type::none(db)), + _ => Type::none(db), }); } diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index 0eaaf037c3..12f40c6e21 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -48,6 +48,13 @@ fn enclosing_generic_contexts<'db>( .last_definition_signature(db) .generic_context } + NodeWithScopeKind::TypeAlias(type_alias) => { + let definition = index.expect_single_definition(type_alias.node(module)); + binding_type(db, definition) + .into_type_alias()? + .into_pep_695_type_alias()? + .generic_context(db) + } _ => None, }) } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index ae1fd7f409..3731dabd83 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -99,16 +99,16 @@ use crate::types::diagnostic::{ INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_NAMED_TUPLE, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, - INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL, - POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, - UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, - report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, - report_implicit_return_type, report_instance_layout_conflict, - report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated, - report_invalid_arguments_to_callable, report_invalid_assignment, - report_invalid_attribute_assignment, report_invalid_generator_function_return_type, - report_invalid_key_on_typed_dict, report_invalid_return_type, - report_namedtuple_field_without_default_after_field_with_default, + INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE, + POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, + UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, + UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_bad_dunder_set_call, + report_cannot_pop_required_field_on_typed_dict, report_implicit_return_type, + report_instance_layout_conflict, report_invalid_argument_number_to_special_form, + report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable, + report_invalid_assignment, report_invalid_attribute_assignment, + report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict, + report_invalid_return_type, report_namedtuple_field_without_default_after_field_with_default, report_possibly_unbound_attribute, }; use crate::types::enums::is_enum_class; @@ -3212,6 +3212,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.db(), &type_alias.name.as_name_expr().unwrap().id, rhs_scope, + None, )), )); @@ -8818,6 +8819,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { if let Type::SpecialForm(SpecialFormType::Tuple) = value_ty { return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice)); } + if let Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) = value_ty { + if let Some(generic_context) = type_alias.generic_context(self.db()) { + return self.infer_explicit_type_alias_specialization( + subscript, + value_ty, + type_alias, + generic_context, + ); + } + } let slice_ty = self.infer_expression(slice); let result_ty = self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); @@ -8830,6 +8841,52 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_ty: Type<'db>, generic_class: ClassLiteral<'db>, generic_context: GenericContext<'db>, + ) -> Type<'db> { + let db = self.db(); + let specialize = |types: &[Option>]| { + Type::from(generic_class.apply_specialization(db, |_| { + generic_context.specialize_partial(db, types.iter().copied()) + })) + }; + + self.infer_explicit_callable_specialization( + subscript, + value_ty, + generic_context, + specialize, + ) + } + + fn infer_explicit_type_alias_specialization( + &mut self, + subscript: &ast::ExprSubscript, + value_ty: Type<'db>, + generic_type_alias: TypeAliasType<'db>, + generic_context: GenericContext<'db>, + ) -> Type<'db> { + let db = self.db(); + let specialize = |types: &[Option>]| { + let type_alias = generic_type_alias.apply_specialization(db, |_| { + generic_context.specialize_partial(db, types.iter().copied()) + }); + + Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) + }; + + self.infer_explicit_callable_specialization( + subscript, + value_ty, + generic_context, + specialize, + ) + } + + fn infer_explicit_callable_specialization( + &mut self, + subscript: &ast::ExprSubscript, + value_ty: Type<'db>, + generic_context: GenericContext<'db>, + specialize: impl FnOnce(&[Option>]) -> Type<'db>, ) -> Type<'db> { let slice_node = subscript.slice.as_ref(); let call_argument_types = match slice_node { @@ -8864,10 +8921,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .matching_overloads() .next() .expect("valid bindings should have matching overload"); - Type::from(generic_class.apply_specialization(self.db(), |_| { - generic_context - .specialize_partial(self.db(), overload.parameter_types().iter().copied()) - })) + + specialize(overload.parameter_types()) } fn infer_subscript_expression_types( @@ -9044,6 +9099,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { Some(todo_type!("doubly-specialized typing.Protocol")) } + ( + Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::PEP695(alias))), + _, + ) if alias.generic_context(db).is_none() => { + if let Some(builder) = self.context.report_lint(&NON_SUBSCRIPTABLE, subscript) { + builder + .into_diagnostic(format_args!("Cannot subscript non-generic type alias")); + } + + Some(Type::unknown()) + } + (Type::SpecialForm(SpecialFormType::Generic), typevars) => Some( self.legacy_generic_class_context(value_node, typevars, LegacyGenericBase::Generic) .map(|context| { @@ -9066,7 +9133,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } (Type::KnownInstance(known_instance), _) - if known_instance.class().is_special_form() => + if known_instance.class(db).is_special_form() => { Some(todo_type!("Inference of subscript on special form")) } @@ -10534,9 +10601,43 @@ impl<'db> TypeInferenceBuilder<'db, '_> { self.infer_type_expression(&subscript.slice); todo_type!("TypeVar annotations") } - KnownInstanceType::TypeAliasType(_) => { - self.infer_type_expression(&subscript.slice); - todo_type!("Generic PEP-695 type alias") + KnownInstanceType::TypeAliasType(type_alias @ TypeAliasType::PEP695(_)) => { + match type_alias.generic_context(self.db()) { + Some(generic_context) => { + let specialized_type_alias = self + .infer_explicit_type_alias_specialization( + subscript, + value_ty, + type_alias, + generic_context, + ); + + specialized_type_alias + .in_type_expression( + self.db(), + self.scope(), + self.typevar_binding_context, + ) + .unwrap_or(Type::unknown()) + } + None => { + self.infer_type_expression(slice); + + if let Some(builder) = + self.context.report_lint(&NON_SUBSCRIPTABLE, subscript) + { + builder.into_diagnostic(format_args!( + "Cannot subscript non-generic type alias" + )); + } + + Type::unknown() + } + } + } + KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695(_)) => { + self.infer_type_expression(slice); + todo_type!("Generic manual PEP-695 type alias") } }, Type::Dynamic(DynamicType::Todo(_)) => { @@ -10552,6 +10653,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { class, generic_context, ); + specialized_class .in_type_expression( self.db(),