mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 20:24:27 +00:00
[ty] Use constructor parameter types as type context (#21054)
## Summary Resolves https://github.com/astral-sh/ty/issues/1408.
This commit is contained in:
parent
c3de8847d5
commit
304ac22e74
4 changed files with 161 additions and 24 deletions
|
|
@ -145,3 +145,84 @@ def h[T](x: T, cond: bool) -> T | list[T]:
|
||||||
def i[T](x: T, cond: bool) -> T | list[T]:
|
def i[T](x: T, cond: bool) -> T | list[T]:
|
||||||
return x if cond else [x]
|
return x if cond else [x]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Type context sources
|
||||||
|
|
||||||
|
Type context is sourced from various places, including annotated assignments:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
a: list[Literal[1]] = [1]
|
||||||
|
```
|
||||||
|
|
||||||
|
Function parameter annotations:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def b(x: list[Literal[1]]): ...
|
||||||
|
|
||||||
|
b([1])
|
||||||
|
```
|
||||||
|
|
||||||
|
Bound method parameter annotations:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C:
|
||||||
|
def __init__(self, x: list[Literal[1]]): ...
|
||||||
|
def foo(self, x: list[Literal[1]]): ...
|
||||||
|
|
||||||
|
C([1]).foo([1])
|
||||||
|
```
|
||||||
|
|
||||||
|
Declared variable types:
|
||||||
|
|
||||||
|
```py
|
||||||
|
d: list[Literal[1]]
|
||||||
|
d = [1]
|
||||||
|
```
|
||||||
|
|
||||||
|
Declared attribute types:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class E:
|
||||||
|
e: list[Literal[1]]
|
||||||
|
|
||||||
|
def _(e: E):
|
||||||
|
# TODO: Implement attribute type context.
|
||||||
|
# error: [invalid-assignment] "Object of type `list[Unknown | int]` is not assignable to attribute `e` of type `list[Literal[1]]`"
|
||||||
|
e.e = [1]
|
||||||
|
```
|
||||||
|
|
||||||
|
Function return types:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f() -> list[Literal[1]]:
|
||||||
|
return [1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Class constructor parameters
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[environment]
|
||||||
|
python-version = "3.12"
|
||||||
|
```
|
||||||
|
|
||||||
|
The parameters of both `__init__` and `__new__` are used as type context sources for constructor
|
||||||
|
calls:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f[T](x: T) -> list[T]:
|
||||||
|
return [x]
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __new__(cls, value: list[int | str]):
|
||||||
|
return super().__new__(cls, value)
|
||||||
|
|
||||||
|
def __init__(self, value: list[int | None]): ...
|
||||||
|
|
||||||
|
A(f(1))
|
||||||
|
|
||||||
|
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `list[int | str]`, found `list[list[Unknown]]`"
|
||||||
|
# error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `list[int | None]`, found `list[list[Unknown]]`"
|
||||||
|
A(f([]))
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -6007,6 +6007,9 @@ impl<'db> Type<'db> {
|
||||||
/// Given a class literal or non-dynamic `SubclassOf` type, try calling it (creating an instance)
|
/// Given a class literal or non-dynamic `SubclassOf` type, try calling it (creating an instance)
|
||||||
/// and return the resulting instance type.
|
/// and return the resulting instance type.
|
||||||
///
|
///
|
||||||
|
/// The `infer_argument_types` closure should be invoked with the signatures of `__new__` and
|
||||||
|
/// `__init__`, such that the argument types can be inferred with the correct type context.
|
||||||
|
///
|
||||||
/// Models `type.__call__` behavior.
|
/// Models `type.__call__` behavior.
|
||||||
/// TODO: model metaclass `__call__`.
|
/// TODO: model metaclass `__call__`.
|
||||||
///
|
///
|
||||||
|
|
@ -6017,10 +6020,10 @@ impl<'db> Type<'db> {
|
||||||
///
|
///
|
||||||
/// Foo()
|
/// Foo()
|
||||||
/// ```
|
/// ```
|
||||||
fn try_call_constructor(
|
fn try_call_constructor<'ast>(
|
||||||
self,
|
self,
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
argument_types: CallArguments<'_, 'db>,
|
infer_argument_types: impl FnOnce(Option<Bindings<'db>>) -> CallArguments<'ast, 'db>,
|
||||||
tcx: TypeContext<'db>,
|
tcx: TypeContext<'db>,
|
||||||
) -> Result<Type<'db>, ConstructorCallError<'db>> {
|
) -> Result<Type<'db>, ConstructorCallError<'db>> {
|
||||||
debug_assert!(matches!(
|
debug_assert!(matches!(
|
||||||
|
|
@ -6076,11 +6079,63 @@ impl<'db> Type<'db> {
|
||||||
// easy to check if that's the one we found?
|
// easy to check if that's the one we found?
|
||||||
// Note that `__new__` is a static method, so we must inject the `cls` argument.
|
// Note that `__new__` is a static method, so we must inject the `cls` argument.
|
||||||
let new_method = self_type.lookup_dunder_new(db, ());
|
let new_method = self_type.lookup_dunder_new(db, ());
|
||||||
|
|
||||||
|
// Construct an instance type that we can use to look up the `__init__` instance method.
|
||||||
|
// This performs the same logic as `Type::to_instance`, except for generic class literals.
|
||||||
|
// TODO: we should use the actual return type of `__new__` to determine the instance type
|
||||||
|
let init_ty = self_type
|
||||||
|
.to_instance(db)
|
||||||
|
.expect("type should be convertible to instance type");
|
||||||
|
|
||||||
|
// Lookup the `__init__` instance method in the MRO.
|
||||||
|
let init_method = init_ty.member_lookup_with_policy(
|
||||||
|
db,
|
||||||
|
"__init__".into(),
|
||||||
|
MemberLookupPolicy::NO_INSTANCE_FALLBACK | MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Infer the call argument types, using both `__new__` and `__init__` for type-context.
|
||||||
|
let bindings = match (
|
||||||
|
new_method.as_ref().map(|method| &method.place),
|
||||||
|
&init_method.place,
|
||||||
|
) {
|
||||||
|
(Some(Place::Defined(new_method, ..)), Place::Undefined) => Some(
|
||||||
|
new_method
|
||||||
|
.bindings(db)
|
||||||
|
.map(|binding| binding.with_bound_type(self_type)),
|
||||||
|
),
|
||||||
|
|
||||||
|
(Some(Place::Undefined) | None, Place::Defined(init_method, ..)) => {
|
||||||
|
Some(init_method.bindings(db))
|
||||||
|
}
|
||||||
|
|
||||||
|
(Some(Place::Defined(new_method, ..)), Place::Defined(init_method, ..)) => {
|
||||||
|
let callable = UnionBuilder::new(db)
|
||||||
|
.add(*new_method)
|
||||||
|
.add(*init_method)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let new_method_bindings = new_method
|
||||||
|
.bindings(db)
|
||||||
|
.map(|binding| binding.with_bound_type(self_type));
|
||||||
|
|
||||||
|
Some(Bindings::from_union(
|
||||||
|
callable,
|
||||||
|
[new_method_bindings, init_method.bindings(db)],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let argument_types = infer_argument_types(bindings);
|
||||||
|
|
||||||
let new_call_outcome = new_method.and_then(|new_method| {
|
let new_call_outcome = new_method.and_then(|new_method| {
|
||||||
match new_method.place.try_call_dunder_get(db, self_type) {
|
match new_method.place.try_call_dunder_get(db, self_type) {
|
||||||
Place::Defined(new_method, _, boundness) => {
|
Place::Defined(new_method, _, boundness) => {
|
||||||
let result =
|
let result =
|
||||||
new_method.try_call(db, argument_types.with_self(Some(self_type)).as_ref());
|
new_method.try_call(db, argument_types.with_self(Some(self_type)).as_ref());
|
||||||
|
|
||||||
if boundness == Definedness::PossiblyUndefined {
|
if boundness == Definedness::PossiblyUndefined {
|
||||||
Some(Err(DunderNewCallError::PossiblyUnbound(result.err())))
|
Some(Err(DunderNewCallError::PossiblyUnbound(result.err())))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -6091,24 +6146,7 @@ impl<'db> Type<'db> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Construct an instance type that we can use to look up the `__init__` instance method.
|
let init_call_outcome = if new_call_outcome.is_none() || !init_method.is_undefined() {
|
||||||
// This performs the same logic as `Type::to_instance`, except for generic class literals.
|
|
||||||
// TODO: we should use the actual return type of `__new__` to determine the instance type
|
|
||||||
let init_ty = self_type
|
|
||||||
.to_instance(db)
|
|
||||||
.expect("type should be convertible to instance type");
|
|
||||||
|
|
||||||
let init_call_outcome = if new_call_outcome.is_none()
|
|
||||||
|| !init_ty
|
|
||||||
.member_lookup_with_policy(
|
|
||||||
db,
|
|
||||||
"__init__".into(),
|
|
||||||
MemberLookupPolicy::NO_INSTANCE_FALLBACK
|
|
||||||
| MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
|
|
||||||
)
|
|
||||||
.place
|
|
||||||
.is_undefined()
|
|
||||||
{
|
|
||||||
Some(init_ty.try_call_dunder(db, "__init__", argument_types, tcx))
|
Some(init_ty.try_call_dunder(db, "__init__", argument_types, tcx))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,14 @@ impl<'db> Bindings<'db> {
|
||||||
self.elements.iter()
|
self.elements.iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map(self, f: impl Fn(CallableBinding<'db>) -> CallableBinding<'db>) -> Self {
|
||||||
|
Self {
|
||||||
|
callable_type: self.callable_type,
|
||||||
|
argument_forms: self.argument_forms,
|
||||||
|
elements: self.elements.into_iter().map(f).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Match the arguments of a call site against the parameters of a collection of possibly
|
/// Match the arguments of a call site against the parameters of a collection of possibly
|
||||||
/// unioned, possibly overloaded signatures.
|
/// unioned, possibly overloaded signatures.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -6798,9 +6798,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
.to_class_type(self.db())
|
.to_class_type(self.db())
|
||||||
.is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class))
|
.is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class))
|
||||||
{
|
{
|
||||||
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
|
|
||||||
self.infer_argument_types(arguments, &mut call_arguments, &argument_forms);
|
|
||||||
|
|
||||||
if matches!(
|
if matches!(
|
||||||
class.known(self.db()),
|
class.known(self.db()),
|
||||||
Some(KnownClass::TypeVar | KnownClass::ExtensionsTypeVar)
|
Some(KnownClass::TypeVar | KnownClass::ExtensionsTypeVar)
|
||||||
|
|
@ -6819,8 +6816,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let db = self.db();
|
||||||
|
let infer_call_arguments = |bindings: Option<Bindings<'db>>| {
|
||||||
|
if let Some(bindings) = bindings {
|
||||||
|
let bindings = bindings.match_parameters(self.db(), &call_arguments);
|
||||||
|
self.infer_all_argument_types(arguments, &mut call_arguments, &bindings);
|
||||||
|
} else {
|
||||||
|
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
|
||||||
|
self.infer_argument_types(arguments, &mut call_arguments, &argument_forms);
|
||||||
|
}
|
||||||
|
|
||||||
|
call_arguments
|
||||||
|
};
|
||||||
|
|
||||||
return callable_type
|
return callable_type
|
||||||
.try_call_constructor(self.db(), call_arguments, tcx)
|
.try_call_constructor(db, infer_call_arguments, tcx)
|
||||||
.unwrap_or_else(|err| {
|
.unwrap_or_else(|err| {
|
||||||
err.report_diagnostic(&self.context, callable_type, call_expression.into());
|
err.report_diagnostic(&self.context, callable_type, call_expression.into());
|
||||||
err.return_type()
|
err.return_type()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue