mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 18:28:56 +00:00
[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:
parent
c16237ddc0
commit
e17cd350b6
15 changed files with 738 additions and 373 deletions
|
@ -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)
|
||||
```
|
||||
|
|
|
@ -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")
|
||||
```
|
||||
|
||||
|
|
|
@ -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")
|
||||
```
|
||||
|
||||
|
|
|
@ -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]
|
||||
```
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
||||
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue