diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md b/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md index 6954499a50895..f8caef3a95042 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/builtins.md @@ -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) ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md index 847d17cd26e39..624fdbfc58714 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/call/methods.md +++ b/crates/red_knot_python_semantic/resources/mdtest/call/methods.md @@ -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") ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md index b384ee820bfee..a9e97b4467657 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md +++ b/crates/red_knot_python_semantic/resources/mdtest/descriptor_protocol.md @@ -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") ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md new file mode 100644 index 0000000000000..d0d3c916df9d2 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/diagnostics/no_matching_overload.md @@ -0,0 +1,14 @@ +# No matching overload 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] +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md index 9bf3007e91fe0..bf2886135431a 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/bool-call.md @@ -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 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md index a0fdd5ca1d9ab..1e3d5f2f1111d 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md +++ b/crates/red_knot_python_semantic/resources/mdtest/narrow/type.md @@ -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 ``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap new file mode 100644 index 0000000000000..c824ee37da98f --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/no_matching_overload.md_-_No_matching_overload_diagnostics_-_Calls_to_overloaded_functions.snap @@ -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 + | + +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 9f33823738ab4..64520da736704 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -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(), - }) - }, - ); + 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 binding = bind_call(db, arguments, &signature, self); + let mut binding = bind_call(db, arguments, overloads(db), self); + let Some((_, overload)) = binding.matching_overload_mut() else { + return Err(CallError::BindingError { binding }); + }; - if binding.has_binding_errors() { - 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 { - Ok(CallOutcome::Single(binding)) + 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 }, + 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, + [KnownClass::Type.to_instance(db), Type::none(db)], + )), + ParameterKind::PositionalOnly { + default_ty: Some(Type::none(db)), + }, + ), + ]), + 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 + ]) + } + + 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__" => { - 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( - 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() - } + overload + .set_return_type(Type::string_literal(db, type_alias.name(db))); } - } else { - Type::unknown() - }, - ), - ); - let binding = bind_call(db, arguments, &signature, self); + ( + 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))); + } - if binding.has_binding_errors() { - Err(CallError::BindingError { binding }) - } else { - Ok(CallOutcome::Single(binding)) + (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() } - // 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)), + 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)), + ), + ]) + } - // 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 + 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)), + ), + ]) + } - Some(KnownClass::Type) => arguments - .exactly_one_argument() - .map(|arg| arg.to_meta_type(db)) - .unwrap_or_else(|| 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() + } - _ => Type::Instance(InstanceType { class }), - }, - ))) + // TODO annotated return type on `__new__` or metaclass `__call__` + // TODO check call vs signatures of `__new__` and/or `__init__` + 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 diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/red_knot_python_semantic/src/types/call.rs index 0fd81de9f4975..475326f95792c 100644 --- a/crates/red_knot_python_semantic/src/types/call.rs +++ b/crates/red_knot_python_semantic/src/types/call.rs @@ -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)) } } } diff --git a/crates/red_knot_python_semantic/src/types/call/arguments.rs b/crates/red_knot_python_semantic/src/types/call/arguments.rs index d5ca096ed9a9b..814db5324be27 100644 --- a/crates/red_knot_python_semantic/src/types/call/arguments.rs +++ b/crates/red_knot_python_semantic/src/types/call/arguments.rs @@ -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> { - 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> { self.0.get(1).map(Argument::ty) diff --git a/crates/red_knot_python_semantic/src/types/call/bind.rs b/crates/red_knot_python_semantic/src/types/call/bind.rs index 208a3e46e1b21..20ff424ac354e 100644 --- a/crates/red_knot_python_semantic/src/types/call/bind.rs +++ b/crates/red_knot_python_semantic/src/types/call/bind.rs @@ -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::>() + .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>, + 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, 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; - } - - pub(crate) fn return_type(&self) -> Type<'db> { - self.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() } - pub(crate) fn parameter_types(&self) -> &[Type<'db>] { - &self.parameter_tys + /// 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()) } - pub(crate) fn one_parameter_type(&self) -> Option> { - match self.parameter_types() { - [ty] => Some(*ty), - _ => None, - } + /// 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()) } - pub(crate) fn two_parameter_types(&self) -> Option<(Type<'db>, Type<'db>)> { - match self.parameter_types() { - [first, second] => Some((*first, *second)), - _ => None, + /// 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> { + if let Some((_, overload)) = self.matching_overload() { + return overload.return_type(); } - } - - 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 { @@ -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>, +} + +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() diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index 8646942c304cd..8a88541d2ad27 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -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. diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 92568640bdfa5..1bdbc43806ae5 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -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); diff --git a/crates/red_knot_python_semantic/src/types/signatures.rs b/crates/red_knot_python_semantic/src/types/signatures.rs index 9acf3552f8389..9d5fa2ba4fbd8 100644 --- a/crates/red_knot_python_semantic/src/types/signatures.rs +++ b/crates/red_knot_python_semantic/src/types/signatures.rs @@ -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(overloads: I) -> Self + where + I: IntoIterator, + I::IntoIter: Iterator>, + { + 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> { + 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> 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); diff --git a/knot.schema.json b/knot.schema.json index 5c757109b1d33..c0d82b5c68757 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -491,6 +491,16 @@ } ] }, + "no-matching-overload": { + "title": "detects calls that do not match any overload", + "description": "## What it does\nChecks for calls to an overloaded function that do not match any of the overloads.\n\n## Why is this bad?\nFailing to provide the correct arguments to one of the overloads will raise a `TypeError`\nat runtime.\n\n## Examples\n```python\n@overload\ndef func(x: int): ...\n@overload\ndef func(x: bool): ...\nfunc(\"string\") # error: [no-matching-overload]\n```", + "default": "error", + "oneOf": [ + { + "$ref": "#/definitions/Level" + } + ] + }, "non-subscriptable": { "title": "detects subscripting objects that do not support subscripting", "description": "## What it does\nChecks for subscripting objects that do not support subscripting.\n\n## Why is this bad?\nSubscripting an object that does not support it will raise a `TypeError` at runtime.\n\n## Examples\n```python\n4[1] # TypeError: 'int' object is not subscriptable\n```",