[ty] Add support for generic PEP695 type aliases (#20219)

## Summary

Adds support for generic PEP695 type aliases, e.g.,
```python
type A[T] = T
reveal_type(A[int]) # A[int]
```

Resolves https://github.com/astral-sh/ty/issues/677.
This commit is contained in:
Ibraheem Ahmed 2025-09-08 16:26:21 -04:00 committed by GitHub
parent d55edb3d74
commit aa5d665d52
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 510 additions and 70 deletions

View file

@ -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" assert_snapshot!(test.hover(), @r"
typing.TypeVar T@Alias
--------------------------------------------- ---------------------------------------------
```python ```python
typing.TypeVar T@Alias
``` ```
--------------------------------------------- ---------------------------------------------
info[hover]: Hovered content is info[hover]: Hovered content is

View file

@ -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
```

View file

@ -40,13 +40,6 @@ You cannot use the same typevar more than once.
class RepeatedTypevar[T, T]: ... 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 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 it with typevars. With PEP 695 syntax, you must explicitly list all of the typevars that you use in
your base classes. your base classes.

View file

@ -188,7 +188,7 @@ T = TypeVar("T")
IntAnd = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,)) IntAnd = TypeAliasType("IntAndT", tuple[int, T], type_params=(T,))
def f(x: IntAnd[str]) -> None: 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 ### Error cases

View file

@ -44,6 +44,7 @@ use crate::types::constraints::{
use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder};
use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION}; use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
pub use crate::types::display::DisplaySettings; pub use crate::types::display::DisplaySettings;
use crate::types::display::TupleSpecialization;
use crate::types::enums::{enum_metadata, is_single_member_enum}; use crate::types::enums::{enum_metadata, is_single_member_enum};
use crate::types::function::{ use crate::types::function::{
DataclassTransformerParams, FunctionSpans, FunctionType, KnownFunction, DataclassTransformerParams, FunctionSpans, FunctionType, KnownFunction,
@ -196,6 +197,11 @@ pub(crate) struct IsEquivalent;
pub(crate) type FindLegacyTypeVarsVisitor<'db> = CycleDetector<FindLegacyTypeVars, Type<'db>, ()>; pub(crate) type FindLegacyTypeVarsVisitor<'db> = CycleDetector<FindLegacyTypeVars, Type<'db>, ()>;
pub(crate) struct FindLegacyTypeVars; pub(crate) struct FindLegacyTypeVars;
/// A [`CycleDetector`] that is used in `try_bool` methods.
pub(crate) type TryBoolVisitor<'db> =
CycleDetector<TryBool, Type<'db>, Result<Truthiness, BoolError<'db>>>;
pub(crate) struct TryBool;
/// A [`TypeTransformer`] that is used in `normalized` methods. /// A [`TypeTransformer`] that is used in `normalized` methods.
pub(crate) type NormalizedVisitor<'db> = TypeTransformer<'db, Normalized>; pub(crate) type NormalizedVisitor<'db> = TypeTransformer<'db, Normalized>;
pub(crate) struct Normalized; pub(crate) struct Normalized;
@ -844,6 +850,13 @@ impl<'db> Type<'db> {
} }
} }
pub(crate) const fn into_type_alias(self) -> Option<TypeAliasType<'db>> {
match self {
Type::KnownInstance(KnownInstanceType::TypeAliasType(type_alias)) => Some(type_alias),
_ => None,
}
}
pub(crate) const fn into_dynamic(self) -> Option<DynamicType> { pub(crate) const fn into_dynamic(self) -> Option<DynamicType> {
match self { match self {
Type::Dynamic(dynamic_type) => Some(dynamic_type), 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. /// 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. /// Use [`try_bool`](Self::try_bool) for type checking or implicit `bool` calls.
pub(crate) fn bool(&self, db: &'db dyn Db) -> Truthiness { 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()) .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. /// Returns an error if the type doesn't implement `__bool__` correctly.
pub(crate) fn try_bool(&self, db: &'db dyn Db) -> Result<Truthiness, BoolError<'db>> { pub(crate) fn try_bool(&self, db: &'db dyn Db) -> Result<Truthiness, BoolError<'db>> {
self.try_bool_impl(db, false) self.try_bool_impl(db, false, &TryBoolVisitor::new(Ok(Truthiness::Ambiguous)))
} }
/// Resolves the boolean value of a type. /// Resolves the boolean value of a type.
@ -3637,6 +3650,7 @@ impl<'db> Type<'db> {
&self, &self,
db: &'db dyn Db, db: &'db dyn Db,
allow_short_circuit: bool, allow_short_circuit: bool,
visitor: &TryBoolVisitor<'db>,
) -> Result<Truthiness, BoolError<'db>> { ) -> Result<Truthiness, BoolError<'db>> {
let type_to_truthiness = |ty| { let type_to_truthiness = |ty| {
if let Type::BooleanLiteral(bool_val) = ty { if let Type::BooleanLiteral(bool_val) = ty {
@ -3706,7 +3720,8 @@ impl<'db> Type<'db> {
let mut has_errors = false; let mut has_errors = false;
for element in union.elements(db) { for element in union.elements(db) {
let element_truthiness = match element.try_bool_impl(db, allow_short_circuit) { let element_truthiness =
match element.try_bool_impl(db, allow_short_circuit, visitor) {
Ok(truthiness) => truthiness, Ok(truthiness) => truthiness,
Err(err) => { Err(err) => {
has_errors = true; has_errors = true;
@ -3768,17 +3783,19 @@ impl<'db> Type<'db> {
Type::AlwaysFalsy => Truthiness::AlwaysFalse, Type::AlwaysFalsy => Truthiness::AlwaysFalse,
Type::ClassLiteral(class) => class Type::ClassLiteral(class) => {
class
.metaclass_instance_type(db) .metaclass_instance_type(db)
.try_bool_impl(db, allow_short_circuit)?, .try_bool_impl(db, allow_short_circuit, visitor)?
}
Type::GenericAlias(alias) => ClassType::from(*alias) Type::GenericAlias(alias) => ClassType::from(*alias)
.metaclass_instance_type(db) .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() { Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() {
SubclassOfInner::Dynamic(_) => Truthiness::Ambiguous, SubclassOfInner::Dynamic(_) => Truthiness::Ambiguous,
SubclassOfInner::Class(class) => { 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) { match bound_typevar.typevar(db).bound_or_constraints(db) {
None => Truthiness::Ambiguous, None => Truthiness::Ambiguous,
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { 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)) => { Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
try_union(constraints)? try_union(constraints)?
@ -3821,9 +3838,11 @@ impl<'db> Type<'db> {
Type::BooleanLiteral(bool) => Truthiness::from(*bool), Type::BooleanLiteral(bool) => Truthiness::from(*bool),
Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()), Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()),
Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()), Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()),
Type::TypeAlias(alias) => alias Type::TypeAlias(alias) => visitor.visit(*self, || {
alias
.value_type(db) .value_type(db)
.try_bool_impl(db, allow_short_circuit)?, .try_bool_impl(db, allow_short_circuit, visitor)
})?,
}; };
Ok(truthiness) 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 { match self {
Self::SubscriptedProtocol(_) | Self::SubscriptedGeneric(_) => KnownClass::SpecialForm, Self::SubscriptedProtocol(_) | Self::SubscriptedGeneric(_) => KnownClass::SpecialForm,
Self::TypeVar(_) => KnownClass::TypeVar, Self::TypeVar(_) => KnownClass::TypeVar,
Self::TypeAliasType(TypeAliasType::PEP695(alias)) if alias.is_specialized(db) => {
KnownClass::GenericAlias
}
Self::TypeAliasType(_) => KnownClass::TypeAliasType, Self::TypeAliasType(_) => KnownClass::TypeAliasType,
Self::Deprecated(_) => KnownClass::Deprecated, Self::Deprecated(_) => KnownClass::Deprecated,
Self::Field(_) => KnownClass::Field, Self::Field(_) => KnownClass::Field,
@ -6857,7 +6879,7 @@ impl<'db> KnownInstanceType<'db> {
} }
fn to_meta_type(self, db: &'db dyn Db) -> Type<'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. /// 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)` /// `typing.TypeAliasType`, so `KnownInstanceType::TypeAliasType(_).instance_fallback(db)`
/// returns `Type::NominalInstance(NominalInstanceType { class: <typing.TypeAliasType> })`. /// returns `Type::NominalInstance(NominalInstanceType { class: <typing.TypeAliasType> })`.
fn instance_fallback(self, db: &dyn Db) -> Type<'_> { 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`. /// Return `true` if this symbol is an instance of `class`.
fn is_instance_of(self, db: &dyn Db, class: ClassType) -> bool { 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 /// Return the repr of the symbol at runtime
@ -6892,7 +6914,16 @@ impl<'db> KnownInstanceType<'db> {
f.write_str("typing.Generic")?; f.write_str("typing.Generic")?;
generic_context.display(self.db).fmt(f) 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 // 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 // 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. // 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<TypeVarBoundOrConstraints<'db>> { fn lazy_bound(self, db: &'db dyn Db) -> Option<TypeVarBoundOrConstraints<'db>> {
let definition = self.definition(db)?; let definition = self.definition(db)?;
let module = parsed_module(db, definition.file(db)).load(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<TypeVarBoundOrConstraints<'db>>,
_count: u32,
_self: TypeVarInstance<'db>,
) -> salsa::CycleRecoveryAction<Option<TypeVarBoundOrConstraints<'db>>> {
salsa::CycleRecoveryAction::Iterate
}
fn lazy_bound_cycle_initial<'db>(
_db: &'db dyn Db,
_self: TypeVarInstance<'db>,
) -> Option<TypeVarBoundOrConstraints<'db>> {
None
}
/// Where a type variable is bound and usable. /// Where a type variable is bound and usable.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub enum BindingContext<'db> { pub enum BindingContext<'db> {
@ -9365,6 +9413,8 @@ pub struct PEP695TypeAliasType<'db> {
pub name: ast::name::Name, pub name: ast::name::Name,
rhs_scope: ScopeId<'db>, rhs_scope: ScopeId<'db>,
specialization: Option<Specialization<'db>>,
} }
// The Salsa heap is tracked separately. // 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 module = parsed_module(db, scope.file(db)).load(db);
let type_alias_stmt_node = scope.node(db).expect_type_alias(); let type_alias_stmt_node = scope.node(db).expect_type_alias();
let definition = self.definition(db); 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<GenericContext<'db>> {
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 { 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<GenericContext<'db>>,
_count: u32,
_self: PEP695TypeAliasType<'db>,
) -> salsa::CycleRecoveryAction<Option<GenericContext<'db>>> {
salsa::CycleRecoveryAction::Iterate
}
fn generic_context_cycle_initial<'db>(
_db: &'db dyn Db,
_self: PEP695TypeAliasType<'db>,
) -> Option<GenericContext<'db>> {
None
}
fn value_type_cycle_recover<'db>( fn value_type_cycle_recover<'db>(
_db: &'db dyn Db, _db: &'db dyn Db,
_value: &Type<'db>, _value: &Type<'db>,
@ -9506,6 +9631,41 @@ impl<'db> TypeAliasType<'db> {
TypeAliasType::ManualPEP695(type_alias) => type_alias.value(db), TypeAliasType::ManualPEP695(type_alias) => type_alias.value(db),
} }
} }
pub(crate) fn into_pep_695_type_alias(self) -> Option<PEP695TypeAliasType<'db>> {
match self {
TypeAliasType::PEP695(type_alias) => Some(type_alias),
TypeAliasType::ManualPEP695(_) => None,
}
}
pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
// 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<Specialization<'db>> {
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. /// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes.

View file

@ -32,8 +32,8 @@ use crate::types::signatures::{Parameter, ParameterForm, Parameters};
use crate::types::tuple::{Tuple, TupleLength, TupleType}; use crate::types::tuple::{Tuple, TupleLength, TupleType};
use crate::types::{ use crate::types::{
BoundMethodType, ClassLiteral, DataclassParams, FieldInstance, KnownClass, KnownInstanceType, BoundMethodType, ClassLiteral, DataclassParams, FieldInstance, KnownClass, KnownInstanceType,
MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType, MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeAliasType, TypeMapping,
WrapperDescriptorKind, enums, ide_support, todo_type, UnionType, WrapperDescriptorKind, enums, ide_support, todo_type,
}; };
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity}; use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
use ruff_python_ast::{self as ast, PythonVersion}; use ruff_python_ast::{self as ast, PythonVersion};
@ -662,6 +662,13 @@ impl<'db> Bindings<'db> {
function_generic_context(bound_method.function(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), _ => Type::none(db),
}); });
} }

View file

@ -48,6 +48,13 @@ fn enclosing_generic_contexts<'db>(
.last_definition_signature(db) .last_definition_signature(db)
.generic_context .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, _ => None,
}) })
} }

View file

@ -99,16 +99,16 @@ use crate::types::diagnostic::{
INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_NAMED_TUPLE, INVALID_DECLARATION, INVALID_GENERIC_CLASS, INVALID_KEY, INVALID_NAMED_TUPLE,
INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL,
INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, POSSIBLY_UNBOUND_IMPLICIT_CALL, INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NON_SUBSCRIPTABLE,
POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, POSSIBLY_UNBOUND_IMPLICIT_CALL, POSSIBLY_UNBOUND_IMPORT, TypeCheckDiagnostics,
UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_IMPORT,
report_bad_dunder_set_call, report_cannot_pop_required_field_on_typed_dict, UNRESOLVED_REFERENCE, UNSUPPORTED_OPERATOR, report_bad_dunder_set_call,
report_implicit_return_type, report_instance_layout_conflict, report_cannot_pop_required_field_on_typed_dict, report_implicit_return_type,
report_invalid_argument_number_to_special_form, report_invalid_arguments_to_annotated, report_instance_layout_conflict, report_invalid_argument_number_to_special_form,
report_invalid_arguments_to_callable, report_invalid_assignment, report_invalid_arguments_to_annotated, report_invalid_arguments_to_callable,
report_invalid_attribute_assignment, report_invalid_generator_function_return_type, report_invalid_assignment, report_invalid_attribute_assignment,
report_invalid_key_on_typed_dict, report_invalid_return_type, report_invalid_generator_function_return_type, report_invalid_key_on_typed_dict,
report_namedtuple_field_without_default_after_field_with_default, report_invalid_return_type, report_namedtuple_field_without_default_after_field_with_default,
report_possibly_unbound_attribute, report_possibly_unbound_attribute,
}; };
use crate::types::enums::is_enum_class; use crate::types::enums::is_enum_class;
@ -3212,6 +3212,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.db(), self.db(),
&type_alias.name.as_name_expr().unwrap().id, &type_alias.name.as_name_expr().unwrap().id,
rhs_scope, rhs_scope,
None,
)), )),
)); ));
@ -8818,6 +8819,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if let Type::SpecialForm(SpecialFormType::Tuple) = value_ty { if let Type::SpecialForm(SpecialFormType::Tuple) = value_ty {
return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice)); 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 slice_ty = self.infer_expression(slice);
let result_ty = self.infer_subscript_expression_types(subscript, value_ty, slice_ty, *ctx); 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>, value_ty: Type<'db>,
generic_class: ClassLiteral<'db>, generic_class: ClassLiteral<'db>,
generic_context: GenericContext<'db>, generic_context: GenericContext<'db>,
) -> Type<'db> {
let db = self.db();
let specialize = |types: &[Option<Type<'db>>]| {
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<Type<'db>>]| {
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>,
) -> Type<'db> { ) -> Type<'db> {
let slice_node = subscript.slice.as_ref(); let slice_node = subscript.slice.as_ref();
let call_argument_types = match slice_node { let call_argument_types = match slice_node {
@ -8864,10 +8921,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.matching_overloads() .matching_overloads()
.next() .next()
.expect("valid bindings should have matching overload"); .expect("valid bindings should have matching overload");
Type::from(generic_class.apply_specialization(self.db(), |_| {
generic_context specialize(overload.parameter_types())
.specialize_partial(self.db(), overload.parameter_types().iter().copied())
}))
} }
fn infer_subscript_expression_types( fn infer_subscript_expression_types(
@ -9044,6 +9099,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Some(todo_type!("doubly-specialized typing.Protocol")) 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( (Type::SpecialForm(SpecialFormType::Generic), typevars) => Some(
self.legacy_generic_class_context(value_node, typevars, LegacyGenericBase::Generic) self.legacy_generic_class_context(value_node, typevars, LegacyGenericBase::Generic)
.map(|context| { .map(|context| {
@ -9066,7 +9133,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
} }
(Type::KnownInstance(known_instance), _) (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")) Some(todo_type!("Inference of subscript on special form"))
} }
@ -10534,9 +10601,43 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
self.infer_type_expression(&subscript.slice); self.infer_type_expression(&subscript.slice);
todo_type!("TypeVar annotations") todo_type!("TypeVar annotations")
} }
KnownInstanceType::TypeAliasType(_) => { KnownInstanceType::TypeAliasType(type_alias @ TypeAliasType::PEP695(_)) => {
self.infer_type_expression(&subscript.slice); match type_alias.generic_context(self.db()) {
todo_type!("Generic PEP-695 type alias") 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(_)) => { Type::Dynamic(DynamicType::Todo(_)) => {
@ -10552,6 +10653,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
class, class,
generic_context, generic_context,
); );
specialized_class specialized_class
.in_type_expression( .in_type_expression(
self.db(), self.db(),