Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -433,17 +433,31 @@ the typevars of the enclosing generic class, and introduce new (distinct) typeva
scope for the method.

```py
from ty_extensions import generic_context
from typing import Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")

class C(Generic[T]):
def method(self, u: U) -> U:
def method(self, u: int) -> int:
return u

def generic_method(self, t: T, u: U) -> U:
return u

reveal_type(generic_context(C)) # revealed: tuple[T]
reveal_type(generic_context(C.method)) # revealed: None
reveal_type(generic_context(C.generic_method)) # revealed: tuple[U]
reveal_type(generic_context(C[int])) # revealed: None
reveal_type(generic_context(C[int].method)) # revealed: None
reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[U]

c: C[int] = C[int]()
reveal_type(c.method("string")) # revealed: Literal["string"]
reveal_type(c.generic_method(1, "string")) # revealed: Literal["string"]
reveal_type(generic_context(c)) # revealed: None
reveal_type(generic_context(c.method)) # revealed: None
reveal_type(generic_context(c.generic_method)) # revealed: tuple[U]
```

## Specializations propagate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,17 +392,32 @@ the typevars of the enclosing generic class, and introduce new (distinct) typeva
scope for the method.

```py
from ty_extensions import generic_context

class C[T]:
def method[U](self, u: U) -> U:
def method(self, u: int) -> int:
return u

def generic_method[U](self, t: T, u: U) -> U:
return u
# error: [unresolved-reference]
def cannot_use_outside_of_method(self, u: U): ...

# TODO: error
def cannot_shadow_class_typevar[T](self, t: T): ...

reveal_type(generic_context(C)) # revealed: tuple[T]
reveal_type(generic_context(C.method)) # revealed: None
reveal_type(generic_context(C.generic_method)) # revealed: tuple[U]
reveal_type(generic_context(C[int])) # revealed: None
reveal_type(generic_context(C[int].method)) # revealed: None
reveal_type(generic_context(C[int].generic_method)) # revealed: tuple[U]

c: C[int] = C[int]()
reveal_type(c.method("string")) # revealed: Literal["string"]
reveal_type(c.generic_method(1, "string")) # revealed: Literal["string"]
reveal_type(generic_context(c)) # revealed: None
reveal_type(generic_context(c.method)) # revealed: None
reveal_type(generic_context(c.generic_method)) # revealed: tuple[U]
```

## Specializations propagate
Expand Down
14 changes: 13 additions & 1 deletion crates/ty_python_semantic/resources/mdtest/generics/scoping.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,22 @@ class Legacy(Generic[T]):
def m(self, x: T, y: S) -> S:
return y

legacy: Legacy[int] = Legacy()
legacy: Legacy[int] = Legacy[int]()
reveal_type(legacy.m(1, "string")) # revealed: Literal["string"]
```

The class typevar in the method signature does not bind a _new_ instance of the typevar; it was
already solved and specialized when the class was specialized:

```py
from ty_extensions import generic_context

legacy.m("string", None) # error: [invalid-argument-type]
reveal_type(legacy.m) # revealed: bound method Legacy[int].m(x: int, y: S) -> S
reveal_type(generic_context(Legacy)) # revealed: tuple[T]
reveal_type(generic_context(legacy.m)) # revealed: tuple[S]
```

With PEP 695 syntax, it is clearer that the method uses a separate typevar:

```py
Expand Down
39 changes: 29 additions & 10 deletions crates/ty_python_semantic/src/types/call/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,19 +623,38 @@ impl<'db> Bindings<'db> {

Some(KnownFunction::GenericContext) => {
if let [Some(ty)] = overload.parameter_types() {
let function_generic_context = |function: FunctionType<'db>| {
let union = UnionType::from_elements(
db,
function
.signature(db)
.overloads
.iter()
.filter_map(|signature| signature.generic_context)
.map(|generic_context| generic_context.as_tuple(db)),
);
if union.is_never() {
Type::none(db)
} else {
union
}
};

// TODO: Handle generic functions, and unions/intersections of
// generic types
overload.set_return_type(match ty {
Type::ClassLiteral(class) => match class.generic_context(db) {
Some(generic_context) => TupleType::from_elements(
db,
generic_context
.variables(db)
.iter()
.map(|typevar| Type::TypeVar(*typevar)),
),
None => Type::none(db),
},
Type::ClassLiteral(class) => class
.generic_context(db)
.map(|generic_context| generic_context.as_tuple(db))
.unwrap_or_else(|| Type::none(db)),

Type::FunctionLiteral(function) => {
function_generic_context(*function)
}

Type::BoundMethod(bound_method) => {
function_generic_context(bound_method.function(db))
}

_ => Type::none(db),
});
Expand Down
1 change: 1 addition & 0 deletions crates/ty_python_semantic/src/types/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ impl<'db> OverloadLiteral<'db> {
let index = semantic_index(db, scope.file(db));
GenericContext::from_type_params(db, index, type_params)
});

Signature::from_function(
db,
generic_context,
Expand Down
70 changes: 68 additions & 2 deletions crates/ty_python_semantic/src/types/generics.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,64 @@
use std::borrow::Cow;

use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast as ast;
use rustc_hash::FxHashMap;

use crate::semantic_index::SemanticIndex;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::scope::{FileScopeId, NodeWithScopeKind};
use crate::semantic_index::{SemanticIndex, semantic_index};
use crate::types::class::ClassType;
use crate::types::class_base::ClassBase;
use crate::types::instance::{NominalInstanceType, Protocol, ProtocolInstanceType};
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::tuple::{TupleSpec, TupleType};
use crate::types::{
KnownInstanceType, Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarBoundOrConstraints,
TypeVarInstance, TypeVarVariance, UnionType, declaration_type,
TypeVarInstance, TypeVarVariance, UnionType, binding_type, declaration_type,
};
use crate::{Db, FxOrderSet};

/// Returns an iterator of any generic context introduced by the given scope or any enclosing
/// scope.
fn enclosing_generic_contexts<'db>(
db: &'db dyn Db,
module: &ParsedModuleRef,
index: &SemanticIndex<'db>,
scope: FileScopeId,
) -> impl Iterator<Item = GenericContext<'db>> {
index
.ancestor_scopes(scope)
.filter_map(|(_, ancestor_scope)| match ancestor_scope.node() {
NodeWithScopeKind::Class(class) => {
binding_type(db, index.expect_single_definition(class.node(module)))
.into_class_literal()?
.generic_context(db)
}
NodeWithScopeKind::Function(function) => {
binding_type(db, index.expect_single_definition(function.node(module)))
.into_function_literal()?
.signature(db)
.iter()
.last()
.expect("function should have at least one overload")
.generic_context
}
_ => None,
})
}

/// Returns the legacy typevars that have been bound in the given scope or any enclosing scope.
fn bound_legacy_typevars<'db>(
db: &'db dyn Db,
module: &ParsedModuleRef,
index: &'db SemanticIndex<'db>,
scope: FileScopeId,
) -> impl Iterator<Item = TypeVarInstance<'db>> {
enclosing_generic_contexts(db, module, index, scope)
.flat_map(|generic_context| generic_context.variables(db).iter().copied())
.filter(|typevar| typevar.is_legacy(db))
}

/// A list of formal type variables for a generic function, class, or type alias.
///
/// TODO: Handle nested generic contexts better, with actual parent links to the lexically
Expand Down Expand Up @@ -82,9 +126,11 @@ impl<'db> GenericContext<'db> {
/// list.
pub(crate) fn from_function_params(
db: &'db dyn Db,
definition: Definition<'db>,
parameters: &Parameters<'db>,
return_type: Option<Type<'db>>,
) -> Option<Self> {
// Find all of the legacy typevars mentioned in the function signature.
let mut variables = FxOrderSet::default();
for param in parameters {
if let Some(ty) = param.annotated_type() {
Expand All @@ -97,6 +143,16 @@ impl<'db> GenericContext<'db> {
if let Some(ty) = return_type {
ty.find_legacy_typevars(db, &mut variables);
}

// Then remove any that were bound in enclosing scopes.
let file = definition.file(db);
let module = parsed_module(db, file).load(db);
let index = semantic_index(db, file);
let containing_scope = definition.file_scope(db);
for typevar in bound_legacy_typevars(db, &module, index, containing_scope) {
variables.remove(&typevar);
}

if variables.is_empty() {
return None;
}
Expand Down Expand Up @@ -171,6 +227,16 @@ impl<'db> GenericContext<'db> {
self.specialize(db, types.into())
}

/// Returns a tuple type of the typevars introduced by this generic context.
pub(crate) fn as_tuple(self, db: &'db dyn Db) -> Type<'db> {
TupleType::from_elements(
db,
self.variables(db)
.iter()
.map(|typevar| Type::TypeVar(*typevar)),
)
}

pub(crate) fn is_subset_of(self, db: &'db dyn Db, other: GenericContext<'db>) -> bool {
self.variables(db).is_subset(other.variables(db))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_python_semantic/src/types/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ impl<'db> Signature<'db> {
}
});
let legacy_generic_context =
GenericContext::from_function_params(db, &parameters, return_ty);
GenericContext::from_function_params(db, definition, &parameters, return_ty);

if generic_context.is_some() && legacy_generic_context.is_some() {
// TODO: Raise a diagnostic!
Expand Down
Loading