Compare commits

...

6 Commits

Author SHA1 Message Date
Douglas Creager
0b31a3be5b maybe 2025-03-27 09:42:34 -04:00
Douglas Creager
b12a72b8dc Move parameter matching into helper type 2025-03-26 21:22:34 -04:00
Douglas Creager
5e74cf07fb tmp 2025-03-26 21:21:30 -04:00
Douglas Creager
b7afaa219a Add special callable for specializing a class 2025-03-25 16:23:16 -04:00
Douglas Creager
8dfb59a2b1 Handle explicit specialization before outputting lints 2025-03-24 14:06:20 -04:00
Douglas Creager
2a47422dd5 Build context for generic classes 2025-03-24 13:13:45 -04:00
10 changed files with 501 additions and 139 deletions

View File

@@ -13,8 +13,6 @@ class C[T]: ...
A class that inherits from a generic class, and fills its type parameters with typevars, is generic:
```py
# TODO: no error
# error: [non-subscriptable]
class D[U](C[U]): ...
```
@@ -22,8 +20,6 @@ A class that inherits from a generic class, but fills its type parameters with c
_not_ generic:
```py
# TODO: no error
# error: [non-subscriptable]
class E(C[int]): ...
```
@@ -57,7 +53,7 @@ class D(C[T]): ...
(Examples `E` and `F` from above do not have analogues in the legacy syntax.)
## Inferring generic class parameters
## Specializing generic classes explicitly
The type parameter can be specified explicitly:
@@ -65,15 +61,54 @@ The type parameter can be specified explicitly:
class C[T]:
x: T
# TODO: no error
# TODO: revealed: C[int]
# error: [non-subscriptable]
reveal_type(C[int]()) # revealed: C
```
The specialization must match the generic types:
```py
# error: [too-many-positional-arguments] "Too many positional arguments to explicit specialization of class `C`: expected 2, got 3"
reveal_type(C[int, int]()) # revealed: Unknown
```
If the type variable has an upper bound, the specialized type must satisfy that bound:
```py
class Bounded[T: int]:
x: T
# TODO: revealed: Bounded[int]
reveal_type(Bounded[int]()) # revealed: Bounded
# TODO: error: [invalid-argument]
reveal_type(Bounded[str]()) # revealed: Bounded
```
If the type variable is constrained, the specialized type must satisfy those constraints:
```py
class Constrained[T: (int, str)]:
x: T
# TODO: revealed: Constrained[int]
reveal_type(Constrained[int]()) # revealed: Constrained
# TODO: revealed: Constrained[str]
reveal_type(Constrained[str]()) # revealed: Constrained
# error: [invalid-argument-type]
reveal_type(Constrained[object]()) # revealed: Unknown
```
## Inferring generic class parameters
We can infer the type parameter from a type context:
```py
class C[T]:
x: T
c: C[int] = C()
# TODO: revealed: C[int]
reveal_type(c) # revealed: C
@@ -131,16 +166,11 @@ propagate through:
class Base[T]:
x: T | None = None
# TODO: no error
# error: [non-subscriptable]
class Sub[U](Base[U]): ...
# TODO: no error
# TODO: revealed: int | None
# error: [non-subscriptable]
reveal_type(Base[int].x) # revealed: T | None
# TODO: revealed: int | None
# error: [non-subscriptable]
reveal_type(Sub[int].x) # revealed: T | None
```
@@ -155,8 +185,6 @@ Here, `Sub` is not a generic class, since it fills its superclass's type paramet
```pyi
class Base[T]: ...
# TODO: no error
# error: [non-subscriptable]
class Sub(Base[Sub]): ...
reveal_type(Sub) # revealed: Literal[Sub]
@@ -169,8 +197,6 @@ A similar case can work in a non-stub file, if forward references are stringifie
```py
class Base[T]: ...
# TODO: no error
# error: [non-subscriptable]
class Sub(Base["Sub"]): ...
reveal_type(Sub) # revealed: Literal[Sub]
@@ -183,8 +209,6 @@ In a non-stub file, without stringified forward references, this raises a `NameE
```py
class Base[T]: ...
# TODO: the unresolved-reference error is correct, the non-subscriptable is not
# error: [non-subscriptable]
# error: [unresolved-reference]
class Sub(Base[Sub]): ...
```

View File

@@ -8,8 +8,6 @@ In type stubs, classes can reference themselves in their base class definitions.
```pyi
class Foo[T]: ...
# TODO: actually is subscriptable
# error: [non-subscriptable]
class Bar(Foo[Bar]): ...
reveal_type(Bar) # revealed: Literal[Bar]

View File

@@ -49,6 +49,7 @@ mod class_base;
mod context;
mod diagnostic;
mod display;
mod generics;
mod infer;
mod mro;
mod narrow;
@@ -655,6 +656,7 @@ impl<'db> Type<'db> {
.to_instance(db)
.is_subtype_of(db, target)
}
(Type::Callable(CallableType::SpecializeClass(_)), _) => self == target,
(
Type::Callable(CallableType::General(self_callable)),
@@ -1036,7 +1038,8 @@ impl<'db> Type<'db> {
| Type::Callable(
CallableType::BoundMethod(..)
| CallableType::MethodWrapperDunderGet(..)
| CallableType::WrapperDescriptorDunderGet,
| CallableType::WrapperDescriptorDunderGet
| CallableType::SpecializeClass(..),
)
| Type::ModuleLiteral(..)
| Type::ClassLiteral(..)
@@ -1050,7 +1053,8 @@ impl<'db> Type<'db> {
| Type::Callable(
CallableType::BoundMethod(..)
| CallableType::MethodWrapperDunderGet(..)
| CallableType::WrapperDescriptorDunderGet,
| CallableType::WrapperDescriptorDunderGet
| CallableType::SpecializeClass(..),
)
| Type::ModuleLiteral(..)
| Type::ClassLiteral(..)
@@ -1250,6 +1254,9 @@ impl<'db> Type<'db> {
Type::Callable(CallableType::WrapperDescriptorDunderGet),
) => !KnownClass::WrapperDescriptorType.is_subclass_of(db, class),
(Type::Callable(CallableType::SpecializeClass(_)), Type::Instance(_))
| (Type::Instance(_), Type::Callable(CallableType::SpecializeClass(_))) => true,
(Type::ModuleLiteral(..), other @ Type::Instance(..))
| (other @ Type::Instance(..), Type::ModuleLiteral(..)) => {
// Modules *can* actually be instances of `ModuleType` subclasses
@@ -1304,7 +1311,8 @@ impl<'db> Type<'db> {
| Type::Callable(
CallableType::BoundMethod(_)
| CallableType::MethodWrapperDunderGet(_)
| CallableType::WrapperDescriptorDunderGet,
| CallableType::WrapperDescriptorDunderGet
| CallableType::SpecializeClass(_),
)
| Type::ModuleLiteral(..)
| Type::IntLiteral(_)
@@ -1374,7 +1382,8 @@ impl<'db> Type<'db> {
| Type::Callable(
CallableType::BoundMethod(_)
| CallableType::MethodWrapperDunderGet(_)
| CallableType::WrapperDescriptorDunderGet,
| CallableType::WrapperDescriptorDunderGet
| CallableType::SpecializeClass(_),
)
| Type::ClassLiteral(..)
| Type::ModuleLiteral(..)
@@ -1423,7 +1432,8 @@ impl<'db> Type<'db> {
| Type::Callable(
CallableType::BoundMethod(..)
| CallableType::MethodWrapperDunderGet(..)
| CallableType::WrapperDescriptorDunderGet,
| CallableType::WrapperDescriptorDunderGet
| CallableType::SpecializeClass(..),
)
| Type::ModuleLiteral(..)
| Type::ClassLiteral(..)
@@ -1486,6 +1496,12 @@ impl<'db> Type<'db> {
Type::Dynamic(_) | Type::Never => Some(Symbol::bound(self).into()),
Type::ClassLiteral(ClassLiteralType { class })
if name == "__class_getitem__" && class.generic_context(db).is_some() =>
{
Some(Symbol::bound(Type::Callable(CallableType::SpecializeClass(*class))).into())
}
Type::ClassLiteral(class_literal @ ClassLiteralType { class }) => {
match (class.known(db), name) {
(Some(KnownClass::FunctionType), "__get__") => Some(
@@ -1635,7 +1651,8 @@ impl<'db> Type<'db> {
.to_instance(db)
.instance_member(db, name)
}
Type::Callable(CallableType::General(_)) => {
Type::Callable(CallableType::SpecializeClass(_))
| Type::Callable(CallableType::General(_)) => {
KnownClass::Object.to_instance(db).instance_member(db, name)
}
@@ -1989,7 +2006,8 @@ impl<'db> Type<'db> {
.to_instance(db)
.member(db, &name)
}
Type::Callable(CallableType::General(_)) => {
Type::Callable(CallableType::SpecializeClass(_))
| Type::Callable(CallableType::General(_)) => {
KnownClass::Object.to_instance(db).member(db, &name)
}
@@ -2438,6 +2456,16 @@ impl<'db> Type<'db> {
Signatures::single(signature)
}
Type::Callable(CallableType::SpecializeClass(class)) => {
let generic_context = class
.generic_context(db)
.expect("should not be able to specialize non-generic class");
Signatures::single(CallableSignature::single(
self,
generic_context.signature(db, Type::class_literal(class)),
))
}
Type::FunctionLiteral(function_type) => match function_type.known(db) {
Some(
KnownFunction::IsEquivalentTo
@@ -3153,7 +3181,8 @@ impl<'db> Type<'db> {
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
KnownClass::WrapperDescriptorType.to_class_literal(db)
}
Type::Callable(CallableType::General(_)) => KnownClass::Type.to_instance(db),
Type::Callable(CallableType::SpecializeClass(_))
| Type::Callable(CallableType::General(_)) => KnownClass::Type.to_instance(db),
Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db),
Type::Tuple(_) => KnownClass::Tuple.to_class_literal(db),
Type::ClassLiteral(ClassLiteralType { class }) => class.metaclass(db),
@@ -4857,6 +4886,9 @@ pub enum CallableType<'db> {
/// type. We currently add this as a separate variant because `FunctionType.__get__`
/// is an overloaded method and we do not support `@overload` yet.
WrapperDescriptorDunderGet,
/// A special callable that specializes a generic class using subscription.
SpecializeClass(Class<'db>),
}
#[salsa::interned(debug)]
@@ -5478,6 +5510,10 @@ impl<'db> TupleType<'db> {
pub fn len(&self, db: &'db dyn Db) -> usize {
self.elements(db).len()
}
pub fn iter(&self, db: &'db dyn Db) -> impl Iterator<Item = Type<'db>> + 'db + '_ {
self.elements(db).iter().copied()
}
}
// Make sure that the `Type` enum does not grow unexpectedly.

View File

@@ -1,7 +1,9 @@
use std::borrow::Cow;
use std::collections::VecDeque;
use std::ops::{Deref, DerefMut};
use super::Type;
use crate::Db;
/// Arguments for a single call, in source order.
#[derive(Clone, Debug, Default)]
@@ -32,6 +34,29 @@ impl<'a> CallArguments<'a> {
pub(crate) fn iter(&self) -> impl Iterator<Item = Argument<'a>> + '_ {
self.0.iter().copied()
}
/// Unpacks any subscript tuple arguments into distinct arguments.
pub(crate) fn unpack_subscript_tuples(&self) -> Cow<'_, CallArguments<'a>> {
// If there are no subscript tuples, we can use the existing argument list as-is.
if self
.0
.iter()
.all(|argument| !matches!(argument, Argument::PositionalSubscriptTuple(_)))
{
return Cow::Borrowed(self);
}
let mut arguments = VecDeque::with_capacity(self.0.len());
for argument in self.iter() {
match argument {
Argument::PositionalSubscriptTuple(count) => {
arguments.extend(std::iter::repeat_n(Argument::Positional, count))
}
_ => arguments.push_back(argument),
}
}
Cow::Owned(CallArguments(arguments))
}
}
impl<'a> FromIterator<Argument<'a>> for CallArguments<'a> {
@@ -46,6 +71,8 @@ pub(crate) enum Argument<'a> {
Synthetic,
/// A positional argument.
Positional,
/// A positional argument that is a packed tuple of multiple subscript expression arguments.
PositionalSubscriptTuple(usize),
/// A starred positional argument (e.g. `*args`).
Variadic,
/// A keyword argument (e.g. `a=1`).
@@ -54,7 +81,23 @@ pub(crate) enum Argument<'a> {
Keywords,
}
impl Argument<'_> {
pub(crate) fn subscript_argument<'db>(
db: &'db dyn Db,
slice_type: Type<'db>,
) -> (Self, Type<'db>) {
match slice_type {
Type::Tuple(tuple) => (
Argument::PositionalSubscriptTuple(tuple.len(db)),
slice_type,
),
_ => (Argument::Positional, slice_type),
}
}
}
/// Arguments for a single call, in source order, along with inferred types for each argument.
#[derive(Clone)]
pub(crate) struct CallArgumentTypes<'a, 'db> {
arguments: CallArguments<'a>,
types: VecDeque<Type<'db>>,
@@ -68,6 +111,16 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> {
Self { arguments, types }
}
/// Create a [`CallArgumentTypes`] from an iterator over non-variadic positional argument
/// types.
pub(crate) fn from_arguments(
arguments: impl IntoIterator<Item = (Argument<'a>, Type<'db>)>,
) -> Self {
let (arguments, types): (VecDeque<_>, VecDeque<_>) = arguments.into_iter().collect();
let arguments = CallArguments(arguments);
Self { arguments, types }
}
/// Create a [`CallArgumentTypes`] from an iterator over non-variadic positional argument
/// types.
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
@@ -112,6 +165,37 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> {
pub(crate) fn iter(&self) -> impl Iterator<Item = (Argument<'a>, Type<'db>)> + '_ {
self.arguments.iter().zip(self.types.iter().copied())
}
/// Unpacks any subscript tuple arguments into distinct arguments.
pub(crate) fn unpack_subscript_tuples(
&self,
db: &'db dyn Db,
) -> Cow<'_, CallArgumentTypes<'a, 'db>> {
// If there are no subscript tuples, we can use the existing argument list as-is.
if self
.arguments
.iter()
.all(|argument| !matches!(argument, Argument::PositionalSubscriptTuple(_)))
{
return Cow::Borrowed(self);
}
let mut types = VecDeque::with_capacity(self.types.len());
for (argument, ty) in self.iter() {
match (argument, ty) {
(Argument::PositionalSubscriptTuple(_), Type::Tuple(tuple)) => {
for ty in tuple.iter(db) {
types.push_back(ty);
}
}
_ => types.push_back(ty),
}
}
Cow::Owned(CallArgumentTypes {
arguments: self.arguments.unpack_subscript_tuples().into_owned(),
types,
})
}
}
impl<'a> Deref for CallArgumentTypes<'a, '_> {

View File

@@ -3,6 +3,8 @@
//! [signatures][crate::types::signatures], we have to handle the fact that the callable might be a
//! union of types, each of which might contain multiple overloads.
use std::borrow::Cow;
use smallvec::SmallVec;
use super::{
@@ -16,7 +18,7 @@ use crate::types::diagnostic::{
NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS,
UNKNOWN_ARGUMENT,
};
use crate::types::signatures::{Parameter, ParameterForm};
use crate::types::signatures::{Parameter, ParameterForm, Parameters};
use crate::types::{
todo_type, BoundMethodType, CallableType, ClassLiteralType, KnownClass, KnownFunction,
KnownInstanceType, UnionType,
@@ -321,6 +323,11 @@ impl<'db> Bindings<'db> {
}
}
Type::Callable(CallableType::SpecializeClass(class)) => {
// TODO: Actually, you know, specialize the class
overload.set_return_type(Type::class_literal(class));
}
Type::FunctionLiteral(function_type) => match function_type.known(db) {
Some(KnownFunction::IsEquivalentTo) => {
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
@@ -542,10 +549,12 @@ impl<'db> CallableBinding<'db> {
// two phases.
//
// [1] https://github.com/python/typing/pull/1839
let callable_type = signature.callable_type;
let overloads = signature
.into_iter()
.map(|signature| {
Binding::match_parameters(
callable_type,
signature,
arguments,
argument_forms,
@@ -572,8 +581,9 @@ impl<'db> CallableBinding<'db> {
// If this callable is a bound method, prepend the self instance onto the arguments list
// before checking.
argument_types.with_self(signature.bound_type, |argument_types| {
let callable_type = signature.callable_type;
for (signature, overload) in signature.iter().zip(&mut self.overloads) {
overload.check_types(db, signature, argument_types);
overload.check_types(db, callable_type, signature, argument_types);
}
});
}
@@ -711,102 +721,190 @@ pub(crate) struct Binding<'db> {
impl<'db> Binding<'db> {
fn match_parameters(
callable_type: Type<'db>,
signature: &Signature<'db>,
arguments: &CallArguments<'_>,
argument_forms: &mut [Option<ParameterForm>],
conflicting_forms: &mut [bool],
) -> Self {
// Special case: you explicitly specialize a generic class via a subscript expression,
// Class[T1, T2, ...]. This ends up calling a `__class_getitem__` method, defined on
// `type`, which specializes the class. Like all subscript expressions, multiple arguments
// are packed into a tuple before calling `__class_getitem__`.
//
// We would rather treat this as `__class_getitem__` taking in a distinct parameter for
// each type variable. This gives us better error messages if parameter matching fails, and
// makes it easier to type-check the arguments against any type parameter bounds or
// constraints.
let unpack_subscript_tuples = matches!(
callable_type,
Type::Callable(CallableType::SpecializeClass(_))
);
BindingBuilder::new(
signature,
arguments.len(),
argument_forms,
conflicting_forms,
unpack_subscript_tuples,
)
.build(arguments)
}
}
struct BindingBuilder<'a, 'db> {
signature: &'a Signature<'db>,
argument_forms: &'a mut [Option<ParameterForm>],
conflicting_forms: &'a mut [bool],
parameters: &'a Parameters<'db>,
/// The parameter that each argument is matched with.
argument_parameters: Vec<Option<usize>>,
/// Whether each parameter has been matched with an argument.
parameter_matched: Vec<bool>,
errors: Vec<BindingError<'db>>,
next_positional: usize,
first_excess_positional: Option<usize>,
num_synthetic_args: usize,
unpack_subscript_tuples: bool,
}
impl<'a, 'db> BindingBuilder<'a, 'db> {
fn new(
signature: &'a Signature<'db>,
argument_count: usize,
argument_forms: &'a mut [Option<ParameterForm>],
conflicting_forms: &'a mut [bool],
unpack_subscript_tuples: bool,
) -> Self {
let parameters = signature.parameters();
// The parameter that each argument is matched with.
let mut argument_parameters = vec![None; arguments.len()];
// Whether each parameter has been matched with an argument.
let mut parameter_matched = vec![false; parameters.len()];
let mut errors = vec![];
let mut next_positional = 0;
let mut first_excess_positional = None;
let mut num_synthetic_args = 0;
let get_argument_index = |argument_index: usize, num_synthetic_args: usize| {
if argument_index >= num_synthetic_args {
// Adjust the argument index to skip synthetic args, which don't appear at the call
// site and thus won't be in the Call node arguments list.
Some(argument_index - num_synthetic_args)
} else {
// we are erroring on a synthetic argument, we'll just emit the diagnostic on the
// entire Call node, since there's no argument node for this argument at the call site
None
}
Self {
signature,
argument_forms,
conflicting_forms,
parameters,
argument_parameters: vec![None; argument_count],
parameter_matched: vec![false; parameters.len()],
errors: vec![],
next_positional: 0,
first_excess_positional: None,
num_synthetic_args: 0,
unpack_subscript_tuples,
}
}
fn get_argument_index(&self, argument_index: usize) -> Option<usize> {
if argument_index >= self.num_synthetic_args {
// Adjust the argument index to skip synthetic args, which don't appear at the call
// site and thus won't be in the Call node arguments list.
Some(argument_index - self.num_synthetic_args)
} else {
// we are erroring on a synthetic argument, we'll just emit the diagnostic on the
// entire Call node, since there's no argument node for this argument at the call site
None
}
}
fn match_positional(&mut self, argument_index: usize, is_synthetic: bool) {
if is_synthetic {
self.num_synthetic_args += 1;
}
let Some((index, parameter)) = self
.parameters
.get_positional(self.next_positional)
.map(|param| (self.next_positional, param))
.or_else(|| self.parameters.variadic())
else {
self.first_excess_positional.get_or_insert(argument_index);
self.next_positional += 1;
return;
};
self.next_positional += 1;
self.assign_parameter(
argument_index,
is_synthetic,
index,
parameter,
!parameter.is_variadic(),
);
}
fn match_keyword(&mut self, argument_index: usize, name: &'a str) {
let Some((index, parameter)) = self
.parameters
.keyword_by_name(name)
.or_else(|| self.parameters.keyword_variadic())
else {
self.errors.push(BindingError::UnknownArgument {
argument_name: ast::name::Name::new(name),
argument_index: self.get_argument_index(argument_index),
});
return;
};
self.assign_parameter(argument_index, false, index, parameter, false);
}
fn assign_parameter(
&mut self,
argument_index: usize,
is_synthetic: bool,
index: usize,
parameter: &Parameter,
positional: bool,
) {
if !is_synthetic {
if let Some(existing) = self.argument_forms[argument_index - self.num_synthetic_args]
.replace(parameter.form)
{
if existing != parameter.form {
self.conflicting_forms[argument_index - self.num_synthetic_args] = true;
}
}
}
if self.parameter_matched[index] {
if !parameter.is_variadic() && !parameter.is_keyword_variadic() {
self.errors.push(BindingError::ParameterAlreadyAssigned {
argument_index: self.get_argument_index(argument_index),
parameter: ParameterContext::new(parameter, index, positional),
});
}
}
self.argument_parameters[argument_index] = Some(index);
self.parameter_matched[index] = true;
}
fn build(mut self, arguments: &'a CallArguments<'_>) -> Binding<'db> {
for (argument_index, argument) in arguments.iter().enumerate() {
let (index, parameter, positional) = match argument {
Argument::Positional | Argument::Synthetic => {
if matches!(argument, Argument::Synthetic) {
num_synthetic_args += 1;
match argument {
Argument::PositionalSubscriptTuple(count) if self.unpack_subscript_tuples => {
for _ in 0..count {
self.match_positional(argument_index, false);
}
let Some((index, parameter)) = parameters
.get_positional(next_positional)
.map(|param| (next_positional, param))
.or_else(|| parameters.variadic())
else {
first_excess_positional.get_or_insert(argument_index);
next_positional += 1;
continue;
};
next_positional += 1;
(index, parameter, !parameter.is_variadic())
}
Argument::Keyword(name) => {
let Some((index, parameter)) = parameters
.keyword_by_name(name)
.or_else(|| parameters.keyword_variadic())
else {
errors.push(BindingError::UnknownArgument {
argument_name: ast::name::Name::new(name),
argument_index: get_argument_index(argument_index, num_synthetic_args),
});
continue;
};
(index, parameter, false)
Argument::Positional | Argument::PositionalSubscriptTuple(_) => {
self.match_positional(argument_index, false)
}
Argument::Synthetic => self.match_positional(argument_index, true),
Argument::Keyword(name) => self.match_keyword(argument_index, name),
Argument::Variadic | Argument::Keywords => {
// TODO
continue;
}
};
if !matches!(argument, Argument::Synthetic) {
if let Some(existing) =
argument_forms[argument_index - num_synthetic_args].replace(parameter.form)
{
if existing != parameter.form {
conflicting_forms[argument_index - num_synthetic_args] = true;
}
}
}
if parameter_matched[index] {
if !parameter.is_variadic() && !parameter.is_keyword_variadic() {
errors.push(BindingError::ParameterAlreadyAssigned {
argument_index: get_argument_index(argument_index, num_synthetic_args),
parameter: ParameterContext::new(parameter, index, positional),
});
}
}
argument_parameters[argument_index] = Some(index);
parameter_matched[index] = true;
}
if let Some(first_excess_argument_index) = first_excess_positional {
errors.push(BindingError::TooManyPositionalArguments {
first_excess_argument_index: get_argument_index(
first_excess_argument_index,
num_synthetic_args,
),
expected_positional_count: parameters.positional().count(),
provided_positional_count: next_positional,
if let Some(first_excess_argument_index) = self.first_excess_positional {
self.errors.push(BindingError::TooManyPositionalArguments {
first_excess_argument_index: self.get_argument_index(first_excess_argument_index),
expected_positional_count: self.parameters.positional().count(),
provided_positional_count: self.next_positional,
});
}
let mut missing = vec![];
for (index, matched) in parameter_matched.iter().copied().enumerate() {
for (index, matched) in self.parameter_matched.iter().copied().enumerate() {
if !matched {
let param = &parameters[index];
let param = &self.parameters[index];
if param.is_variadic()
|| param.is_keyword_variadic()
|| param.default_type().is_some()
@@ -819,25 +917,46 @@ impl<'db> Binding<'db> {
}
if !missing.is_empty() {
errors.push(BindingError::MissingArguments {
self.errors.push(BindingError::MissingArguments {
parameters: ParameterContexts(missing),
});
}
Self {
return_ty: signature.return_ty.unwrap_or(Type::unknown()),
argument_parameters: argument_parameters.into_boxed_slice(),
parameter_tys: vec![None; parameters.len()].into_boxed_slice(),
errors,
Binding {
return_ty: self.signature.return_ty.unwrap_or(Type::unknown()),
argument_parameters: self.argument_parameters.into_boxed_slice(),
parameter_tys: vec![None; self.parameters.len()].into_boxed_slice(),
errors: self.errors,
}
}
}
impl<'db> Binding<'db> {
fn check_types(
&mut self,
db: &'db dyn Db,
callable_type: Type<'db>,
signature: &Signature<'db>,
argument_types: &CallArgumentTypes<'_, 'db>,
) {
// Special case: you explicitly specialize a generic class via a subscript expression,
// Class[T1, T2, ...]. This ends up calling a `__class_getitem__` method, defined on
// `type`, which specializes the class. Like all subscript expressions, multiple arguments
// are packed into a tuple before calling `__class_getitem__`.
//
// We would rather treat this as `__class_getitem__` taking in a distinct parameter for
// each type variable. This gives us better error messages if parameter matching fails, and
// makes it easier to type-check the arguments against any type parameter bounds or
// constraints.
let argument_types = if matches!(
callable_type,
Type::Callable(CallableType::SpecializeClass(_))
) {
argument_types.unpack_subscript_tuples(db)
} else {
Cow::Borrowed(argument_types)
};
let parameters = signature.parameters();
let mut num_synthetic_args = 0;
let get_argument_index = |argument_index: usize, num_synthetic_args: usize| {
@@ -949,6 +1068,10 @@ impl<'db> CallableDescription<'db> {
kind: "wrapper descriptor",
name: "FunctionType.__get__",
}),
Type::Callable(CallableType::SpecializeClass(class)) => Some(CallableDescription {
kind: "explicit specialization of class",
name: class.name(db),
}),
_ => None,
}
}

View File

@@ -27,6 +27,7 @@ use super::{
KnownFunction, Mro, MroError, MroIterator, SubclassOfType, Truthiness, Type, TypeAliasType,
TypeQualifiers, TypeVarInstance,
};
use crate::types::generics::GenericContext;
/// Representation of a runtime class object.
///
@@ -38,6 +39,7 @@ pub struct Class<'db> {
#[return_ref]
pub(crate) name: ast::name::Name,
pub(crate) generic_context: Option<GenericContext<'db>>,
body_scope: ScopeId<'db>,
pub(crate) known: Option<KnownClass>,

View File

@@ -110,6 +110,13 @@ impl Display for DisplayRepresentation<'_> {
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
f.write_str("<wrapper-descriptor `__get__` of `function` objects>")
}
Type::Callable(CallableType::SpecializeClass(class)) => {
write!(
f,
"<specialize-generic of `{class}`>",
class = class.name(self.db)
)
}
Type::Union(union) => union.display(self.db).fmt(f),
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
Type::IntLiteral(n) => n.fmt(f),

View File

@@ -0,0 +1,77 @@
use ruff_python_ast as ast;
use crate::semantic_index::SemanticIndex;
use crate::types::signatures::{Parameter, Parameters, Signature};
use crate::types::{
declaration_type, KnownInstanceType, Type, TypeVarBoundOrConstraints, TypeVarInstance,
UnionType,
};
use crate::Db;
/// A list of formal type variables for a generic function, class, or type alias.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct GenericContext<'db> {
variables: Box<[TypeVarInstance<'db>]>,
}
impl<'db> GenericContext<'db> {
pub(crate) fn from_type_params(
db: &'db dyn Db,
index: &'db SemanticIndex<'db>,
type_params_node: &ast::TypeParams,
) -> Self {
let variables = type_params_node
.iter()
.filter_map(|type_param| Self::variable_from_type_param(db, index, type_param))
.collect();
Self { variables }
}
fn variable_from_type_param(
db: &'db dyn Db,
index: &'db SemanticIndex<'db>,
type_param_node: &ast::TypeParam,
) -> Option<TypeVarInstance<'db>> {
match type_param_node {
ast::TypeParam::TypeVar(node) => {
let definition = index.definition(node);
let Type::KnownInstance(KnownInstanceType::TypeVar(typevar)) =
declaration_type(db, definition).inner_type()
else {
panic!("typevar should be inferred as a TypeVarInstance");
};
Some(typevar)
}
// TODO: Support these!
ast::TypeParam::ParamSpec(_) => None,
ast::TypeParam::TypeVarTuple(_) => None,
}
}
pub(crate) fn signature(&self, db: &'db dyn Db, class: Type<'db>) -> Signature<'db> {
let parameters = Parameters::new(
std::iter::once(Parameter::positional_only(None).with_annotated_type(class)).chain(
self.variables
.iter()
.map(|typevar| Self::parameter_from_typevar(db, typevar)),
),
);
Signature::new(parameters, None)
}
fn parameter_from_typevar(db: &'db dyn Db, typevar: &TypeVarInstance<'db>) -> Parameter<'db> {
let mut parameter = Parameter::positional_only(Some(typevar.name(db).clone()));
match typevar.bound_or_constraints(db) {
Some(TypeVarBoundOrConstraints::UpperBound(_)) => {
// TODO: This should be TypeForm[bound]
parameter = parameter.with_annotated_type(Type::any());
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
parameter = parameter
.with_annotated_type(UnionType::from_elements(db, constraints.iter(db)));
}
None => {}
}
parameter
}
}

View File

@@ -64,6 +64,7 @@ use crate::symbol::{
typing_extensions_symbol, Boundness, LookupError,
};
use crate::types::call::{Argument, Bindings, CallArgumentTypes, CallArguments, CallError};
use crate::types::class::{ClassLiteralType, MetaclassErrorKind};
use crate::types::diagnostic::{
report_implicit_return_type, report_invalid_arguments_to_annotated,
report_invalid_arguments_to_callable, report_invalid_assignment,
@@ -76,15 +77,15 @@ use crate::types::diagnostic::{
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_IMPORT, UNDEFINED_REVEAL,
UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
};
use crate::types::generics::GenericContext;
use crate::types::mro::MroErrorKind;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
class::MetaclassErrorKind, todo_type, Class, DynamicType, FunctionType, InstanceType,
IntersectionBuilder, IntersectionType, KnownClass, KnownFunction, KnownInstanceType,
MetaclassCandidate, Parameter, ParameterForm, Parameters, SliceLiteralType, SubclassOfType,
Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
UnionType,
todo_type, Class, DynamicType, FunctionType, InstanceType, IntersectionBuilder,
IntersectionType, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, Parameter,
ParameterForm, Parameters, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers,
Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay,
TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
};
use crate::types::{CallableType, GeneralCallableType, Signature};
use crate::unpack::Unpack;
@@ -1633,6 +1634,10 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_decorator(decorator);
}
let generic_context = type_params.as_ref().map(|type_params| {
GenericContext::from_type_params(self.db(), self.index, type_params)
});
let body_scope = self
.index
.node_scope(NodeWithScopeRef::Class(class_node))
@@ -1640,7 +1645,13 @@ impl<'db> TypeInferenceBuilder<'db> {
let maybe_known_class = KnownClass::try_from_file_and_name(self.db(), self.file(), name);
let class = Class::new(self.db(), &name.id, body_scope, maybe_known_class);
let class = Class::new(
self.db(),
&name.id,
generic_context,
body_scope,
maybe_known_class,
);
let class_ty = Type::class_literal(class);
self.add_declaration_with_binding(
@@ -1963,7 +1974,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let tuple = TupleType::new(
self.db(),
elts.iter()
.map(|expr| self.infer_type_expression(expr))
.map(|expr| self.infer_expression(expr))
.collect::<Box<_>>(),
);
let constraints = TypeVarBoundOrConstraints::Constraints(tuple);
@@ -5700,11 +5711,11 @@ impl<'db> TypeInferenceBuilder<'db> {
// If the class defines `__getitem__`, return its return type.
//
// See: https://docs.python.org/3/reference/datamodel.html#class-getitem-versus-getitem
match value_ty.try_call_dunder(
let arguments = CallArgumentTypes::from_arguments([Argument::subscript_argument(
self.db(),
"__getitem__",
CallArgumentTypes::positional([slice_ty]),
) {
slice_ty,
)]);
match value_ty.try_call_dunder(self.db(), "__getitem__", arguments) {
Ok(outcome) => return outcome.return_type(self.db()),
Err(err @ CallDunderError::PossiblyUnbound { .. }) => {
self.context.report_lint(
@@ -5763,30 +5774,24 @@ impl<'db> TypeInferenceBuilder<'db> {
);
}
match ty.try_call(
self.db(),
CallArgumentTypes::positional([value_ty, slice_ty]),
) {
let arguments = CallArgumentTypes::from_arguments([
(Argument::Synthetic, value_ty),
Argument::subscript_argument(self.db(), slice_ty),
]);
match ty.try_call(self.db(), arguments) {
Ok(bindings) => return bindings.return_type(self.db()),
Err(CallError(_, bindings)) => {
self.context.report_lint(
&CALL_NON_CALLABLE,
value_node,
format_args!(
"Method `__class_getitem__` of type `{}` is not callable on object of type `{}`",
bindings.callable_type().display(self.db()),
value_ty.display(self.db()),
),
);
bindings.report_diagnostics(&self.context, value_node.into());
return bindings.return_type(self.db());
}
}
}
}
if matches!(value_ty, Type::ClassLiteral(class_literal) if class_literal.class().is_known(self.db(), KnownClass::Type))
{
return KnownClass::GenericAlias.to_instance(self.db());
if let Type::ClassLiteral(ClassLiteralType { class }) = value_ty {
if class.is_known(self.db(), KnownClass::Type) {
return KnownClass::GenericAlias.to_instance(self.db());
}
}
report_non_subscriptable(
@@ -5809,6 +5814,10 @@ impl<'db> TypeInferenceBuilder<'db> {
// TODO: proper support for generic classes
// For now, just infer `Sequence`, if we see something like `Sequence[str]`. This allows us
// to look up attributes on generic base classes, even if we don't understand generics yet.
// Note that this isn't handled by the clause up above for generic classes
// that use legacy type variables and an explicit `Generic` base class.
// Once we handle legacy typevars, this special case will be removed in
// favor of the specialization logic above.
value_ty
}
_ => Type::unknown(),
@@ -7490,7 +7499,7 @@ mod tests {
check_typevar("T", None, None, None);
check_typevar("U", Some("A"), None, None);
check_typevar("V", None, Some(&["A", "B"]), None);
check_typevar("V", None, Some(&["Literal[A]", "Literal[B]"]), None);
check_typevar("W", None, None, Some("A"));
check_typevar("X", Some("A"), None, Some("A1"));

View File

@@ -82,6 +82,8 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
) => Ordering::Equal,
(Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => Ordering::Less,
(_, Type::Callable(CallableType::WrapperDescriptorDunderGet)) => Ordering::Greater,
(Type::Callable(CallableType::SpecializeClass(_)), _) => Ordering::Less,
(_, Type::Callable(CallableType::SpecializeClass(_))) => Ordering::Greater,
(Type::Callable(CallableType::General(_)), Type::Callable(CallableType::General(_))) => {
Ordering::Equal