[red-knot] Support multiple overloads when binding parameters at call sites (#16568)

This updates the `Signature` and `CallBinding` machinery to support
multiple overloads for a callable. This is currently only used for
`KnownFunction`s that we special-case in our type inference code. It
does **_not_** yet update the semantic index builder to handle
`@overload` decorators and construct a multi-signature `Overloads`
instance for real Python functions.

While I was here, I updated many of the `try_call` special cases to use
signatures (possibly overloaded ones now) and `bind_call` to check
parameter lists. We still need some of the mutator methods on
`OverloadBinding` for the special cases where we need to update return
types based on some Rust code.
This commit is contained in:
Douglas Creager 2025-03-11 15:08:17 -04:00 committed by GitHub
parent c16237ddc0
commit e17cd350b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 738 additions and 373 deletions

View file

@ -6,7 +6,7 @@
class NotBool:
__bool__ = None
# TODO: We should emit an `invalid-argument` error here for `2` because `bool` only takes one argument.
# error: [too-many-positional-arguments] "Too many positional arguments to class `bool`: expected 1, got 2"
bool(1, 2)
# TODO: We should emit an `unsupported-bool-conversion` error here because the argument doesn't implement `__bool__` correctly.
@ -29,9 +29,12 @@ But a three-argument call to type creates a dynamic instance of the `type` class
reveal_type(type("Foo", (), {})) # revealed: type
```
Other numbers of arguments are invalid (TODO -- these should emit a diagnostic)
Other numbers of arguments are invalid
```py
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", ())
# error: [no-matching-overload] "No overload of class `type` matches arguments"
type("Foo", (), {}, weird_other_arg=42)
```

View file

@ -235,23 +235,23 @@ method_wrapper(C(), None)
method_wrapper(None, C)
# Passing `None` without an `owner` argument is an
# error: [missing-argument] "No argument provided for required parameter `owner`"
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
method_wrapper(None)
# Passing something that is not assignable to `type` as the `owner` argument is an
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type`"
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
method_wrapper(None, 1)
# Passing `None` as the `owner` argument when `instance` is `None` is an
# error: [invalid-argument-type] "Object of type `None` cannot be assigned to parameter 2 (`owner`) of method wrapper `__get__` of function `f`; expected type `type`"
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
method_wrapper(None, None)
# Calling `__get__` without any arguments is an
# error: [missing-argument] "No argument provided for required parameter `instance`"
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
method_wrapper()
# Calling `__get__` with too many positional arguments is an
# error: [too-many-positional-arguments] "Too many positional arguments to method wrapper `__get__` of function `f`: expected 2, got 3"
# error: [no-matching-overload] "No overload of method wrapper `__get__` of function `f` matches arguments"
method_wrapper(C(), C, "one too many")
```

View file

@ -710,30 +710,30 @@ Finally, we test some error cases for the call to the wrapper descriptor:
```py
# Calling the wrapper descriptor without any arguments is an
# error: [missing-argument] "No arguments provided for required parameters `self`, `instance`"
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
wrapper_descriptor()
# Calling it without the `instance` argument is an also an
# error: [missing-argument] "No argument provided for required parameter `instance`"
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
wrapper_descriptor(f)
# Calling it without the `owner` argument if `instance` is not `None` is an
# error: [missing-argument] "No argument provided for required parameter `owner`"
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
wrapper_descriptor(f, None)
# But calling it with an instance is fine (in this case, the `owner` argument is optional):
wrapper_descriptor(f, C())
# Calling it with something that is not a `FunctionType` as the first argument is an
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 1 (`self`) of wrapper descriptor `FunctionType.__get__`; expected type `FunctionType`"
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
wrapper_descriptor(1, None, type(f))
# Calling it with something that is not a `type` as the `owner` argument is an
# error: [invalid-argument-type] "Object of type `Literal[f]` cannot be assigned to parameter 3 (`owner`) of wrapper descriptor `FunctionType.__get__`; expected type `type`"
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
wrapper_descriptor(f, None, f)
# Calling it with too many positional arguments is an
# error: [too-many-positional-arguments] "Too many positional arguments to wrapper descriptor `FunctionType.__get__`: expected 3, got 4"
# error: [no-matching-overload] "No overload of wrapper descriptor `FunctionType.__get__` matches arguments"
wrapper_descriptor(f, None, type(f), "one too many")
```

View file

@ -0,0 +1,14 @@
# No matching overload diagnostics
<!-- snapshot-diagnostics -->
## Calls to overloaded functions
TODO: Note that we do not yet support the `@overload` decorator to define overloaded functions in
real Python code. We are instead testing a special-cased function where we create an overloaded
signature internally. Update this to an `@overload` function in the Python snippet itself once we
can.
```py
type("Foo", ()) # error: [no-matching-overload]
```

View file

@ -22,11 +22,13 @@ def _(flag: bool):
# invalid invocation, too many positional args
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None, 5): # TODO diagnostic
# error: [too-many-positional-arguments] "Too many positional arguments to class `bool`: expected 1, got 2"
if bool(x is not None, 5):
reveal_type(x) # revealed: Literal[1] | None
# invalid invocation, too many kwargs
reveal_type(x) # revealed: Literal[1] | None
if bool(x is not None, y=5): # TODO diagnostic
# error: [unknown-argument] "Argument `y` does not match any known parameter of class `bool`"
if bool(x is not None, y=5):
reveal_type(x) # revealed: Literal[1] | None
```

View file

@ -88,7 +88,7 @@ def _(x: str | int):
```py
def _(x: str | int):
# TODO: we could issue a diagnostic here
# error: [no-matching-overload] "No overload of class `type` matches arguments"
if type(object=x) is str:
reveal_type(x) # revealed: str | int
```

View file

@ -0,0 +1,28 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: no_matching_overload.md - No matching overload diagnostics - Calls to overloaded functions
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md
---
# Python source files
## mdtest_snippet.py
```
1 | type("Foo", ()) # error: [no-matching-overload]
```
# Diagnostics
```
error: lint:no-matching-overload
--> /src/mdtest_snippet.py:1:1
|
1 | type("Foo", ()) # error: [no-matching-overload]
| ^^^^^^^^^^^^^^^ No overload of class `type` matches arguments
|
```

View file

@ -20,7 +20,7 @@ pub(crate) use self::infer::{
infer_scope_types,
};
pub use self::narrow::KnownConstraintFunction;
pub(crate) use self::signatures::Signature;
pub(crate) use self::signatures::{CallableSignature, Signature};
pub use self::subclass_of::SubclassOfType;
use crate::module_name::ModuleName;
use crate::module_resolver::{file_to_module, resolve_module, KnownModule};
@ -30,7 +30,7 @@ use crate::semantic_index::symbol::ScopeId;
use crate::semantic_index::{imported_modules, semantic_index};
use crate::suppression::check_suppressions;
use crate::symbol::{imported_symbol, Boundness, Symbol, SymbolAndQualifiers};
use crate::types::call::{bind_call, CallArguments, CallBinding, CallOutcome, UnionCallError};
use crate::types::call::{bind_call, CallArguments, CallOutcome, UnionCallError};
use crate::types::class_base::ClassBase;
use crate::types::diagnostic::{INVALID_TYPE_FORM, UNSUPPORTED_BOOL_CONVERSION};
use crate::types::infer::infer_unpack_types;
@ -2306,19 +2306,13 @@ impl<'db> Type<'db> {
Type::Callable(CallableType::BoundMethod(bound_method)) => {
let instance = bound_method.self_instance(db);
let arguments = arguments.with_self(instance);
let binding = bind_call(
db,
&arguments,
bound_method.function(db).signature(db),
self,
);
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
binding.into_outcome()
}
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
// Here, we dynamically model the overloaded function signature of `types.FunctionType.__get__`.
@ -2334,299 +2328,311 @@ impl<'db> Type<'db> {
// def __get__(self, instance: object, owner: type | None = None, /) -> MethodType: ...
// ```
let first_argument_is_none =
arguments.first_argument().is_some_and(|ty| ty.is_none(db));
let signature = Signature::new(
Parameters::new([
Parameter::new(
Some("instance".into()),
Some(Type::object(db)),
ParameterKind::PositionalOnly { default_ty: None },
#[salsa::tracked(return_ref)]
fn overloads<'db>(db: &'db dyn Db) -> CallableSignature<'db> {
let not_none = Type::none(db).negate(db);
CallableSignature::from_overloads([
Signature::new(
Parameters::new([
Parameter::new(
Some(Name::new_static("instance")),
Some(Type::none(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some(Name::new_static("owner")),
Some(KnownClass::Type.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
]),
None,
),
if first_argument_is_none {
Parameter::new(
Some("owner".into()),
Some(KnownClass::Type.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
)
} else {
Parameter::new(
Some("owner".into()),
Some(UnionType::from_elements(
db,
[KnownClass::Type.to_instance(db), Type::none(db)],
)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::none(db)),
},
)
},
]),
if function.has_known_class_decorator(db, KnownClass::Classmethod)
&& function.decorators(db).len() == 1
{
if let Some(owner) = arguments.second_argument() {
Some(Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, owner),
)))
} else if let Some(instance) = arguments.first_argument() {
Some(Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, instance.to_meta_type(db)),
)))
} else {
Some(Type::unknown())
}
} else {
Some(match arguments.first_argument() {
Some(ty) if ty.is_none(db) => Type::FunctionLiteral(function),
Some(instance) => Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, instance),
)),
_ => Type::unknown(),
})
},
);
let binding = bind_call(db, arguments, &signature, self);
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
Signature::new(
Parameters::new([
Parameter::new(
Some(Name::new_static("instance")),
Some(not_none),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some(Name::new_static("owner")),
Some(UnionType::from_elements(
db,
[KnownClass::Type.to_instance(db), Type::none(db)],
)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::none(db)),
},
),
]),
None,
),
])
}
let mut binding = bind_call(db, arguments, overloads(db), self);
let Some((_, overload)) = binding.matching_overload_mut() else {
return Err(CallError::BindingError { binding });
};
if function.has_known_class_decorator(db, KnownClass::Classmethod)
&& function.decorators(db).len() == 1
{
if let Some(owner) = arguments.second_argument() {
overload.set_return_type(Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, owner),
)));
} else if let Some(instance) = arguments.first_argument() {
overload.set_return_type(Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, instance.to_meta_type(db)),
)));
}
} else {
if let Some(first) = arguments.first_argument() {
if first.is_none(db) {
overload.set_return_type(Type::FunctionLiteral(function));
} else {
overload.set_return_type(Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, first),
)));
}
}
}
binding.into_outcome()
}
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
// Here, we also model `types.FunctionType.__get__`, but now we consider a call to
// this as a function, i.e. we also expect the `self` argument to be passed in.
let second_argument_is_none =
arguments.second_argument().is_some_and(|ty| ty.is_none(db));
let signature = Signature::new(
Parameters::new([
Parameter::new(
Some("self".into()),
Some(KnownClass::FunctionType.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
// TODO: Consider merging this signature with the one in the previous match clause,
// since the previous one is just this signature with the `self` parameters
// removed.
#[salsa::tracked(return_ref)]
fn overloads<'db>(db: &'db dyn Db) -> CallableSignature<'db> {
let not_none = Type::none(db).negate(db);
CallableSignature::from_overloads([
Signature::new(
Parameters::new([
Parameter::new(
Some(Name::new_static("self")),
Some(KnownClass::FunctionType.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some(Name::new_static("instance")),
Some(Type::none(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some(Name::new_static("owner")),
Some(KnownClass::Type.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
]),
None,
),
Parameter::new(
Some("instance".into()),
Some(Type::object(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
if second_argument_is_none {
Parameter::new(
Some("owner".into()),
Some(KnownClass::Type.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
)
} else {
Parameter::new(
Some("owner".into()),
Some(UnionType::from_elements(
db,
[KnownClass::Type.to_instance(db), Type::none(db)],
)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::none(db)),
},
)
},
]),
Some(
if let Some(function_ty @ Type::FunctionLiteral(function)) =
arguments.first_argument()
{
if function.has_known_class_decorator(db, KnownClass::Classmethod)
&& function.decorators(db).len() == 1
{
if let Some(owner) = arguments.third_argument() {
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
db, function, owner,
)))
} else if let Some(instance) = arguments.second_argument() {
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
Signature::new(
Parameters::new([
Parameter::new(
Some(Name::new_static("self")),
Some(KnownClass::FunctionType.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some(Name::new_static("instance")),
Some(not_none),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some(Name::new_static("owner")),
Some(UnionType::from_elements(
db,
function,
instance.to_meta_type(db),
)))
} else {
Type::unknown()
}
} else {
if let Some(instance) = arguments.second_argument() {
if instance.is_none(db) {
function_ty
} else {
match instance {
Type::KnownInstance(
KnownInstanceType::TypeAliasType(type_alias),
) if arguments
.third_argument()
.and_then(Type::into_class_literal)
.is_some_and(|class_literal| {
class_literal
.class
.is_known(db, KnownClass::TypeAliasType)
})
&& function.name(db) == "__name__" =>
{
Type::string_literal(db, type_alias.name(db))
}
Type::KnownInstance(KnownInstanceType::TypeVar(
typevar,
)) if arguments
.third_argument()
.and_then(Type::into_class_literal)
.is_some_and(|class_literal| {
class_literal
.class
.is_known(db, KnownClass::TypeVar)
})
&& function.name(db) == "__name__" =>
{
Type::string_literal(db, typevar.name(db))
}
_ => {
if function.has_known_class_decorator(
db,
KnownClass::Property,
) {
todo_type!("@property")
} else {
Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(
db, function, instance,
),
))
}
}
}
}
} else {
Type::unknown()
}
}
} else {
Type::unknown()
},
),
);
let binding = bind_call(db, arguments, &signature, self);
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
[KnownClass::Type.to_instance(db), Type::none(db)],
)),
ParameterKind::PositionalOnly {
default_ty: Some(Type::none(db)),
},
),
]),
None,
),
])
}
let mut binding = bind_call(db, arguments, overloads(db), self);
let Some((_, overload)) = binding.matching_overload_mut() else {
return Err(CallError::BindingError { binding });
};
if let Some(function_ty @ Type::FunctionLiteral(function)) =
arguments.first_argument()
{
if function.has_known_class_decorator(db, KnownClass::Classmethod)
&& function.decorators(db).len() == 1
{
if let Some(owner) = arguments.third_argument() {
overload.set_return_type(Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, owner),
)));
} else if let Some(instance) = arguments.second_argument() {
overload.set_return_type(Type::Callable(CallableType::BoundMethod(
BoundMethodType::new(db, function, instance.to_meta_type(db)),
)));
}
} else {
match (arguments.second_argument(), arguments.third_argument()) {
(Some(instance), _) if instance.is_none(db) => {
overload.set_return_type(function_ty);
}
(
Some(Type::KnownInstance(KnownInstanceType::TypeAliasType(
type_alias,
))),
Some(Type::ClassLiteral(ClassLiteralType { class })),
) if class.is_known(db, KnownClass::TypeAliasType)
&& function.name(db) == "__name__" =>
{
overload
.set_return_type(Type::string_literal(db, type_alias.name(db)));
}
(
Some(Type::KnownInstance(KnownInstanceType::TypeVar(typevar))),
Some(Type::ClassLiteral(ClassLiteralType { class })),
) if class.is_known(db, KnownClass::TypeVar)
&& function.name(db) == "__name__" =>
{
overload
.set_return_type(Type::string_literal(db, typevar.name(db)));
}
(Some(_), _)
if function.has_known_class_decorator(db, KnownClass::Property) =>
{
overload.set_return_type(todo_type!("@property"));
}
(Some(instance), _) => {
overload.set_return_type(Type::Callable(
CallableType::BoundMethod(BoundMethodType::new(
db, function, instance,
)),
));
}
(None, _) => {}
}
}
}
binding.into_outcome()
}
Type::FunctionLiteral(function_type) => {
let mut binding = bind_call(db, arguments, function_type.signature(db), self);
if binding.has_binding_errors() {
let Some((_, overload)) = binding.matching_overload_mut() else {
return Err(CallError::BindingError { binding });
}
};
match function_type.known(db) {
Some(KnownFunction::IsEquivalentTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding
.set_return_type(Type::BooleanLiteral(ty_a.is_equivalent_to(db, ty_b)));
if let [ty_a, ty_b] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
ty_a.is_equivalent_to(db, *ty_b),
));
}
}
Some(KnownFunction::IsSubtypeOf) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(ty_a.is_subtype_of(db, ty_b)));
if let [ty_a, ty_b] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
ty_a.is_subtype_of(db, *ty_b),
));
}
}
Some(KnownFunction::IsAssignableTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding
.set_return_type(Type::BooleanLiteral(ty_a.is_assignable_to(db, ty_b)));
if let [ty_a, ty_b] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
ty_a.is_assignable_to(db, *ty_b),
));
}
}
Some(KnownFunction::IsDisjointFrom) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding
.set_return_type(Type::BooleanLiteral(ty_a.is_disjoint_from(db, ty_b)));
if let [ty_a, ty_b] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
ty_a.is_disjoint_from(db, *ty_b),
));
}
}
Some(KnownFunction::IsGradualEquivalentTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_gradual_equivalent_to(db, ty_b),
));
if let [ty_a, ty_b] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(
ty_a.is_gradual_equivalent_to(db, *ty_b),
));
}
}
Some(KnownFunction::IsFullyStatic) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
if let [ty] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
}
}
Some(KnownFunction::IsSingleton) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
if let [ty] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
}
}
Some(KnownFunction::IsSingleValued) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db)));
if let [ty] = overload.parameter_types() {
overload.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db)));
}
}
Some(KnownFunction::Len) => {
if let Some(first_arg) = binding.one_parameter_type() {
if let [first_arg] = overload.parameter_types() {
if let Some(len_ty) = first_arg.len(db) {
binding.set_return_type(len_ty);
overload.set_return_type(len_ty);
}
};
}
Some(KnownFunction::Repr) => {
if let Some(first_arg) = binding.one_parameter_type() {
binding.set_return_type(first_arg.repr(db));
if let [first_arg] = overload.parameter_types() {
overload.set_return_type(first_arg.repr(db));
};
}
Some(KnownFunction::Cast) => {
// TODO: Use `.two_parameter_tys()` exclusively
// when overloads are supported.
// TODO: Use `.parameter_types()` exclusively when overloads are supported.
if let Some(casted_ty) = arguments.first_argument() {
if binding.two_parameter_types().is_some() {
binding.set_return_type(casted_ty);
if let [_, _] = overload.parameter_types() {
overload.set_return_type(casted_ty);
}
};
}
Some(KnownFunction::Overload) => {
binding.set_return_type(todo_type!("overload(..) return type"));
overload.set_return_type(todo_type!("overload(..) return type"));
}
Some(KnownFunction::GetattrStatic) => {
let Some((instance_ty, attr_name, default)) =
binding.three_parameter_types()
else {
return Ok(CallOutcome::Single(binding));
let [instance_ty, attr_name, default] = overload.parameter_types() else {
return binding.into_outcome();
};
let Some(attr_name) = attr_name.into_string_literal() else {
return Ok(CallOutcome::Single(binding));
return binding.into_outcome();
};
let default = if default.is_unknown() {
Type::Never
} else {
default
*default
};
let union_with_default = |ty| UnionType::from_elements(db, [ty, default]);
// TODO: we could emit a diagnostic here (if default is not set)
binding.set_return_type(
overload.set_return_type(
match instance_ty.static_member(db, attr_name.value(db)) {
Symbol::Type(ty, Boundness::Bound) => {
if instance_ty.is_fully_static(db) {
@ -2651,50 +2657,173 @@ impl<'db> Type<'db> {
_ => {}
};
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
binding.into_outcome()
}
Type::ClassLiteral(ClassLiteralType { class })
if class.is_known(db, KnownClass::Bool) =>
{
// ```py
// class bool(int):
// def __new__(cls, o: object = ..., /) -> Self: ...
// ```
#[salsa::tracked(return_ref)]
fn overloads<'db>(db: &'db dyn Db) -> CallableSignature<'db> {
Signature::new(
Parameters::new([Parameter::new(
Some(Name::new_static("o")),
Some(Type::any()),
ParameterKind::PositionalOnly {
default_ty: Some(Type::BooleanLiteral(false)),
},
)]),
Some(KnownClass::Bool.to_instance(db)),
)
.into()
}
let mut binding = bind_call(db, arguments, overloads(db), self);
let Some((_, overload)) = binding.matching_overload_mut() else {
return Err(CallError::BindingError { binding });
};
overload.set_return_type(
arguments
.first_argument()
.map(|arg| arg.bool(db).into_type(db))
.unwrap_or(Type::BooleanLiteral(false)),
);
binding.into_outcome()
}
Type::ClassLiteral(ClassLiteralType { class })
if class.is_known(db, KnownClass::Str) =>
{
// ```py
// class str(Sequence[str]):
// @overload
// def __new__(cls, object: object = ...) -> Self: ...
// @overload
// def __new__(cls, object: ReadableBuffer, encoding: str = ..., errors: str = ...) -> Self: ...
// ```
#[salsa::tracked(return_ref)]
fn overloads<'db>(db: &'db dyn Db) -> CallableSignature<'db> {
CallableSignature::from_overloads([
Signature::new(
Parameters::new([Parameter::new(
Some(Name::new_static("o")),
Some(Type::any()),
ParameterKind::PositionalOnly {
default_ty: Some(Type::string_literal(db, "")),
},
)]),
Some(KnownClass::Str.to_instance(db)),
),
Signature::new(
Parameters::new([
Parameter::new(
Some(Name::new_static("o")),
Some(Type::any()), // TODO: ReadableBuffer
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some(Name::new_static("encoding")),
Some(KnownClass::Str.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some(Name::new_static("errors")),
Some(KnownClass::Str.to_instance(db)),
ParameterKind::PositionalOnly { default_ty: None },
),
]),
Some(KnownClass::Str.to_instance(db)),
),
])
}
let mut binding = bind_call(db, arguments, overloads(db), self);
let Some((index, overload)) = binding.matching_overload_mut() else {
return Err(CallError::BindingError { binding });
};
if index == 0 {
overload.set_return_type(
arguments
.first_argument()
.map(|arg| arg.str(db))
.unwrap_or_else(|| Type::string_literal(db, "")),
);
}
binding.into_outcome()
}
Type::ClassLiteral(ClassLiteralType { class })
if class.is_known(db, KnownClass::Type) =>
{
// ```py
// class type:
// @overload
// def __init__(self, o: object, /) -> None: ...
// @overload
// def __init__(self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None: ...
// ```
#[salsa::tracked(return_ref)]
fn overloads<'db>(db: &'db dyn Db) -> CallableSignature<'db> {
CallableSignature::from_overloads([
Signature::new(
Parameters::new([Parameter::new(
Some(Name::new_static("o")),
Some(Type::any()),
ParameterKind::PositionalOnly { default_ty: None },
)]),
Some(KnownClass::Type.to_instance(db)),
),
Signature::new(
Parameters::new([
Parameter::new(
Some(Name::new_static("o")),
Some(Type::any()),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some(Name::new_static("bases")),
Some(Type::any()),
ParameterKind::PositionalOnly { default_ty: None },
),
Parameter::new(
Some(Name::new_static("dict")),
Some(Type::any()),
ParameterKind::PositionalOnly { default_ty: None },
),
]),
Some(KnownClass::Type.to_instance(db)),
),
])
}
let mut binding = bind_call(db, arguments, overloads(db), self);
let Some((index, overload)) = binding.matching_overload_mut() else {
return Err(CallError::BindingError { binding });
};
if index == 0 {
if let Some(arg) = arguments.first_argument() {
overload.set_return_type(arg.to_meta_type(db));
}
}
binding.into_outcome()
}
// TODO annotated return type on `__new__` or metaclass `__call__`
// TODO check call vs signatures of `__new__` and/or `__init__`
Type::ClassLiteral(ClassLiteralType { class }) => {
Ok(CallOutcome::Single(CallBinding::from_return_type(
match class.known(db) {
// TODO: We should check the call signature and error if the bool call doesn't have the
// right signature and return a binding error.
// If the class is the builtin-bool class (for example `bool(1)`), we try to
// return the specific truthiness value of the input arg, `Literal[True]` for
// the example above.
Some(KnownClass::Bool) => arguments
.first_argument()
.map(|arg| arg.bool(db).into_type(db))
.unwrap_or(Type::BooleanLiteral(false)),
// TODO: Don't ignore the second and third arguments to `str`
// https://github.com/astral-sh/ruff/pull/16161#discussion_r1958425568
Some(KnownClass::Str) => arguments
.first_argument()
.map(|arg| arg.str(db))
.unwrap_or_else(|| Type::string_literal(db, "")),
Some(KnownClass::Type) => arguments
.exactly_one_argument()
.map(|arg| arg.to_meta_type(db))
.unwrap_or_else(|| KnownClass::Type.to_instance(db)),
_ => Type::Instance(InstanceType { class }),
},
)))
Type::ClassLiteral(ClassLiteralType { .. }) => {
let signature = Signature::new(Parameters::gradual_form(), self.to_instance(db));
let binding = bind_call(db, arguments, &signature.into(), self);
binding.into_outcome()
}
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
ClassBase::Dynamic(dynamic_type) => Ok(CallOutcome::Single(
CallBinding::from_return_type(Type::Dynamic(dynamic_type)),
)),
ClassBase::Dynamic(dynamic_type) => {
Type::Dynamic(dynamic_type).try_call(db, arguments)
}
ClassBase::Class(class) => Type::class_literal(class).try_call(db, arguments),
},
@ -2739,16 +2868,20 @@ impl<'db> Type<'db> {
// Dynamic types are callable, and the return type is the same dynamic type. Similarly,
// `Never` is always callable and returns `Never`.
Type::Dynamic(_) | Type::Never => {
Ok(CallOutcome::Single(CallBinding::from_return_type(self)))
let overloads = CallableSignature::dynamic(self);
let binding = bind_call(db, arguments, &overloads, self);
binding.into_outcome()
}
Type::Union(union) => {
CallOutcome::try_call_union(db, union, |element| element.try_call(db, arguments))
}
Type::Intersection(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(
todo_type!("Type::Intersection.call()"),
))),
Type::Intersection(_) => {
let overloads = CallableSignature::todo("Type::Intersection.call()");
let binding = bind_call(db, arguments, &overloads, self);
binding.into_outcome()
}
_ => Err(CallError::NotCallable {
not_callable_type: self,
@ -4189,8 +4322,8 @@ impl<'db> FunctionType<'db> {
/// Were this not a salsa query, then the calling query
/// would depend on the function's AST and rerun for every change in that file.
#[salsa::tracked(return_ref)]
pub fn signature(self, db: &'db dyn Db) -> Signature<'db> {
let internal_signature = self.internal_signature(db);
pub fn signature(self, db: &'db dyn Db) -> CallableSignature<'db> {
let internal_signature = self.internal_signature(db).into();
let decorators = self.decorators(db);
let mut decorators = decorators.iter();
@ -4202,7 +4335,7 @@ impl<'db> FunctionType<'db> {
{
internal_signature
} else {
Signature::todo("return type of decorated function")
CallableSignature::todo("return type of decorated function")
}
} else {
internal_signature

View file

@ -1,5 +1,5 @@
use super::context::InferContext;
use super::{Signature, Type};
use super::{CallableSignature, Signature, Type};
use crate::types::UnionType;
use crate::Db;
@ -70,7 +70,7 @@ impl<'db> CallOutcome<'db> {
match self {
Self::Single(binding) => binding.return_type(),
Self::Union(bindings) => {
UnionType::from_elements(db, bindings.iter().map(bind::CallBinding::return_type))
UnionType::from_elements(db, bindings.iter().map(CallBinding::return_type))
}
}
}

View file

@ -35,14 +35,6 @@ impl<'a, 'db> CallArguments<'a, 'db> {
self.0.first().map(Argument::ty)
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn exactly_one_argument(&self) -> Option<Type<'db>> {
match &*self.0 {
[arg] => Some(arg.ty()),
_ => None,
}
}
// TODO this should be eliminated in favor of [`bind_call`]
pub(crate) fn second_argument(&self) -> Option<Type<'db>> {
self.0.get(1).map(Argument::ty)

View file

@ -1,25 +1,50 @@
use super::{Argument, CallArguments, InferContext, Signature, Type};
use super::{
Argument, CallArguments, CallError, CallOutcome, CallableSignature, InferContext, Signature,
Type,
};
use crate::db::Db;
use crate::types::diagnostic::{
INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, PARAMETER_ALREADY_ASSIGNED,
INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED,
TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT,
};
use crate::types::signatures::Parameter;
use crate::types::{todo_type, CallableType, UnionType};
use crate::types::{CallableType, UnionType};
use ruff_db::diagnostic::{OldSecondaryDiagnosticMessage, Span};
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
/// Bind a [`CallArguments`] against a callable [`Signature`].
/// Bind a [`CallArguments`] against a [`CallableSignature`].
///
/// The returned [`CallBinding`] provides the return type of the call, the bound types for all
/// parameters, and any errors resulting from binding the call.
pub(crate) fn bind_call<'db>(
db: &'db dyn Db,
arguments: &CallArguments<'_, 'db>,
signature: &Signature<'db>,
overloads: &CallableSignature<'db>,
callable_ty: Type<'db>,
) -> CallBinding<'db> {
// TODO: This checks every overload. In the proposed more detailed call checking spec [1],
// arguments are checked for arity first, and are only checked for type assignability against
// the matching overloads. Make sure to implement that as part of separating call binding into
// two phases.
//
// [1] https://github.com/python/typing/pull/1839
let overloads = overloads
.iter()
.map(|signature| bind_overload(db, arguments, signature))
.collect::<Vec<_>>()
.into_boxed_slice();
CallBinding {
callable_ty,
overloads,
}
}
fn bind_overload<'db>(
db: &'db dyn Db,
arguments: &CallArguments<'_, 'db>,
signature: &Signature<'db>,
) -> OverloadBinding<'db> {
let parameters = signature.parameters();
// The type assigned to each parameter at this call site.
let mut parameter_tys = vec![None; parameters.len()];
@ -126,8 +151,7 @@ pub(crate) fn bind_call<'db>(
});
}
CallBinding {
callable_ty,
OverloadBinding {
return_ty: signature.return_ty.unwrap_or(Type::unknown()),
parameter_tys: parameter_tys
.into_iter()
@ -144,67 +168,77 @@ pub(crate) struct CallableDescriptor<'a> {
kind: &'a str,
}
/// Binding information for a call site.
///
/// For a successful binding, each argument is mapped to one of the callable's formal parameters.
/// If the callable has multiple overloads, the first one that matches is used as the overall
/// binding match.
///
/// TODO: Implement the call site evaluation algorithm in the [proposed updated typing
/// spec][overloads], which is much more subtle than “first match wins”.
///
/// If the arguments cannot be matched to formal parameters, we store information about the
/// specific errors that occurred when trying to match them up. If the callable has multiple
/// overloads, we store this error information for each overload.
///
/// [overloads]: https://github.com/python/typing/pull/1839
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CallBinding<'db> {
/// Type of the callable object (function, class...)
callable_ty: Type<'db>,
/// Return type of the call.
return_ty: Type<'db>,
/// Bound types for parameters, in parameter source order.
parameter_tys: Box<[Type<'db>]>,
/// Call binding errors, if any.
errors: Vec<CallBindingError<'db>>,
overloads: Box<[OverloadBinding<'db>]>,
}
impl<'db> CallBinding<'db> {
// TODO remove this constructor and construct always from `bind_call`
pub(crate) fn from_return_type(return_ty: Type<'db>) -> Self {
Self {
callable_ty: todo_type!("CallBinding::from_return_type"),
return_ty,
parameter_tys: Box::default(),
errors: vec![],
pub(crate) fn into_outcome(self) -> Result<CallOutcome<'db>, CallError<'db>> {
if self.has_binding_errors() {
return Err(CallError::BindingError { binding: self });
}
Ok(CallOutcome::Single(self))
}
pub(crate) fn callable_type(&self) -> Type<'db> {
self.callable_ty
}
pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) {
self.return_ty = return_ty;
/// Returns whether there were any errors binding this call site. If the callable has multiple
/// overloads, they must _all_ have errors.
pub(crate) fn has_binding_errors(&self) -> bool {
self.matching_overload().is_none()
}
/// Returns the overload that matched for this call binding. Returns `None` if none of the
/// overloads matched.
pub(crate) fn matching_overload(&self) -> Option<(usize, &OverloadBinding<'db>)> {
self.overloads
.iter()
.enumerate()
.find(|(_, overload)| !overload.has_binding_errors())
}
/// Returns the overload that matched for this call binding. Returns `None` if none of the
/// overloads matched.
pub(crate) fn matching_overload_mut(&mut self) -> Option<(usize, &mut OverloadBinding<'db>)> {
self.overloads
.iter_mut()
.enumerate()
.find(|(_, overload)| !overload.has_binding_errors())
}
/// Returns the return type of this call. For a valid call, this is the return type of the
/// overload that the arguments matched against. For an invalid call to a non-overloaded
/// function, this is the return type of the function. For an invalid call to an overloaded
/// function, we return `Type::unknown`, since we cannot make any useful conclusions about
/// which overload was intended to be called.
pub(crate) fn return_type(&self) -> Type<'db> {
self.return_ty
}
pub(crate) fn parameter_types(&self) -> &[Type<'db>] {
&self.parameter_tys
}
pub(crate) fn one_parameter_type(&self) -> Option<Type<'db>> {
match self.parameter_types() {
[ty] => Some(*ty),
_ => None,
if let Some((_, overload)) = self.matching_overload() {
return overload.return_type();
}
}
pub(crate) fn two_parameter_types(&self) -> Option<(Type<'db>, Type<'db>)> {
match self.parameter_types() {
[first, second] => Some((*first, *second)),
_ => None,
}
}
pub(crate) fn three_parameter_types(&self) -> Option<(Type<'db>, Type<'db>, Type<'db>)> {
match self.parameter_types() {
[first, second, third] => Some((*first, *second, *third)),
_ => None,
if let [overload] = self.overloads.as_ref() {
return overload.return_type();
}
Type::unknown()
}
fn callable_descriptor(&self, db: &'db dyn Db) -> Option<CallableDescriptor> {
@ -235,10 +269,30 @@ impl<'db> CallBinding<'db> {
}
}
/// Report diagnostics for all of the errors that occurred when trying to match actual
/// arguments to formal parameters. If the callable has multiple overloads, we report a single
/// diagnostic that we couldn't match any overload.
/// TODO: Update this to add subdiagnostics about how we failed to match each overload.
pub(crate) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
let callable_descriptor = self.callable_descriptor(context.db());
for error in &self.errors {
error.report_diagnostic(
if self.overloads.len() > 1 {
context.report_lint(
&NO_MATCHING_OVERLOAD,
node,
format_args!(
"No overload{} matches arguments",
if let Some(CallableDescriptor { kind, name }) = callable_descriptor {
format!(" of {kind} `{name}`")
} else {
String::new()
}
),
);
return;
}
for overload in &self.overloads {
overload.report_diagnostics(
context,
node,
self.callable_ty,
@ -246,6 +300,45 @@ impl<'db> CallBinding<'db> {
);
}
}
}
/// Binding information for one of the overloads of a callable.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct OverloadBinding<'db> {
/// Return type of the call.
return_ty: Type<'db>,
/// Bound types for parameters, in parameter source order.
parameter_tys: Box<[Type<'db>]>,
/// Call binding errors, if any.
errors: Vec<CallBindingError<'db>>,
}
impl<'db> OverloadBinding<'db> {
pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) {
self.return_ty = return_ty;
}
pub(crate) fn return_type(&self) -> Type<'db> {
self.return_ty
}
pub(crate) fn parameter_types(&self) -> &[Type<'db>] {
&self.parameter_tys
}
fn report_diagnostics(
&self,
context: &InferContext<'db>,
node: ast::AnyNodeRef,
callable_ty: Type<'db>,
callable_descriptor: Option<&CallableDescriptor>,
) {
for error in &self.errors {
error.report_diagnostic(context, node, callable_ty, callable_descriptor);
}
}
pub(crate) fn has_binding_errors(&self) -> bool {
!self.errors.is_empty()

View file

@ -45,6 +45,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_TYPE_FORM);
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
registry.register_lint(&MISSING_ARGUMENT);
registry.register_lint(&NO_MATCHING_OVERLOAD);
registry.register_lint(&NON_SUBSCRIPTABLE);
registry.register_lint(&NOT_ITERABLE);
registry.register_lint(&UNSUPPORTED_BOOL_CONVERSION);
@ -474,6 +475,29 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for calls to an overloaded function that do not match any of the overloads.
///
/// ## Why is this bad?
/// Failing to provide the correct arguments to one of the overloads will raise a `TypeError`
/// at runtime.
///
/// ## Examples
/// ```python
/// @overload
/// def func(x: int): ...
/// @overload
/// def func(x: bool): ...
/// func("string") # error: [no-matching-overload]
/// ```
pub(crate) static NO_MATCHING_OVERLOAD = {
summary: "detects calls that do not match any overload",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// ## What it does
/// Checks for subscripting objects that do not support subscripting.

View file

@ -3408,9 +3408,13 @@ impl<'db> TypeInferenceBuilder<'db> {
continue;
};
let Some((_, overload)) = binding.matching_overload() else {
continue;
};
match known_function {
KnownFunction::RevealType => {
if let Some(revealed_type) = binding.one_parameter_type() {
if let [revealed_type] = overload.parameter_types() {
self.context.report_diagnostic(
call_expression,
DiagnosticId::RevealedType,
@ -3424,7 +3428,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
KnownFunction::AssertType => {
if let [actual_ty, asserted_ty] = binding.parameter_types() {
if let [actual_ty, asserted_ty] = overload.parameter_types() {
if !actual_ty.is_gradual_equivalent_to(self.db(), *asserted_ty) {
self.context.report_lint(
&TYPE_ASSERTION_FAILURE,
@ -3439,7 +3443,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
KnownFunction::StaticAssert => {
if let Some((parameter_ty, message)) = binding.two_parameter_types() {
if let [parameter_ty, message] = overload.parameter_types() {
let truthiness = match parameter_ty.try_bool(self.db()) {
Ok(truthiness) => truthiness,
Err(err) => {
@ -3470,7 +3474,7 @@ impl<'db> TypeInferenceBuilder<'db> {
call_expression,
format_args!("Static assertion error: {message}"),
);
} else if parameter_ty == Type::BooleanLiteral(false) {
} else if *parameter_ty == Type::BooleanLiteral(false) {
self.context.report_lint(
&STATIC_ASSERT_ERROR,
call_expression,
@ -6135,8 +6139,8 @@ impl<'db> TypeInferenceBuilder<'db> {
Signature::new(parameters, Some(return_type)),
)));
// `Signature` / `Parameters` are not a `Type` variant, so we're storing the outer
// callable type on the these expressions instead.
// `Signature` / `Parameters` are not a `Type` variant, so we're storing
// the outer callable type on the these expressions instead.
self.store_expression_type(arguments_slice, callable_type);
self.store_expression_type(first_argument, callable_type);

View file

@ -1,9 +1,80 @@
//! _Signatures_ describe the expected parameters and return type of a function or other callable.
//! Overloads and unions add complexity to this simple description.
//!
//! In a call expression, the type of the callable might be a union of several types. The call must
//! be compatible with _all_ of these types, since at runtime the callable might be an instance of
//! any of them.
//!
//! Each of the atomic types in the union must be callable. Each callable might be _overloaded_,
//! containing multiple _overload signatures_, each of which describes a different combination of
//! argument types and return types. For each callable type in the union, the call expression's
//! arguments must match _at least one_ overload.
use super::{definition_expression_type, DynamicType, Type};
use crate::Db;
use crate::{semantic_index::definition::Definition, types::todo_type};
use ruff_python_ast::{self as ast, name::Name};
/// A typed callable signature.
/// The signature of a single callable. If the callable is overloaded, there is a separate
/// [`Signature`] for each overload.
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
pub enum CallableSignature<'db> {
Single(Signature<'db>),
Overloaded(Box<[Signature<'db>]>),
}
impl<'db> CallableSignature<'db> {
/// Creates a new `CallableSignature` from an non-empty iterator of [`Signature`]s.
/// Panics if the iterator is empty.
pub(crate) fn from_overloads<I>(overloads: I) -> Self
where
I: IntoIterator,
I::IntoIter: Iterator<Item = Signature<'db>>,
{
let mut iter = overloads.into_iter();
let first_overload = iter.next().expect("overloads should not be empty");
let Some(second_overload) = iter.next() else {
return CallableSignature::Single(first_overload);
};
let mut overloads = vec![first_overload, second_overload];
overloads.extend(iter);
CallableSignature::Overloaded(overloads.into())
}
pub(crate) fn iter(&self) -> std::slice::Iter<Signature<'db>> {
match self {
CallableSignature::Single(signature) => std::slice::from_ref(signature).iter(),
CallableSignature::Overloaded(signatures) => signatures.iter(),
}
}
/// Return a signature for a dynamic callable
pub(crate) fn dynamic(ty: Type<'db>) -> Self {
let signature = Signature {
parameters: Parameters::gradual_form(),
return_ty: Some(ty),
};
signature.into()
}
/// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo
#[allow(unused_variables)] // 'reason' only unused in debug builds
pub(crate) fn todo(reason: &'static str) -> Self {
let signature = Signature {
parameters: Parameters::todo(),
return_ty: Some(todo_type!(reason)),
};
signature.into()
}
}
impl<'db> From<Signature<'db>> for CallableSignature<'db> {
fn from(signature: Signature<'db>) -> Self {
CallableSignature::Single(signature)
}
}
/// The signature of one of the overloads of a callable.
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
pub struct Signature<'db> {
/// Parameters, in source order.
@ -28,15 +99,6 @@ impl<'db> Signature<'db> {
}
}
/// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo
#[allow(unused_variables)] // 'reason' only unused in debug builds
pub(crate) fn todo(reason: &'static str) -> Self {
Self {
parameters: Parameters::todo(),
return_ty: Some(todo_type!(reason)),
}
}
/// Return a typed signature from a function definition.
pub(super) fn from_function(
db: &'db dyn Db,
@ -722,7 +784,7 @@ mod tests {
.unwrap();
let func = get_function_f(&db, "/src/a.py");
let expected_sig = func.internal_signature(&db);
let expected_sig = func.internal_signature(&db).into();
// With no decorators, internal and external signature are the same
assert_eq!(func.signature(&db), &expected_sig);
@ -743,7 +805,7 @@ mod tests {
.unwrap();
let func = get_function_f(&db, "/src/a.py");
let expected_sig = Signature::todo("return type of decorated function");
let expected_sig = CallableSignature::todo("return type of decorated function");
// With no decorators, internal and external signature are the same
assert_eq!(func.signature(&db), &expected_sig);