diff --git a/crates/ty_ide/src/signature_help.rs b/crates/ty_ide/src/signature_help.rs index d3038e3fb1..b8232ebff2 100644 --- a/crates/ty_ide/src/signature_help.rs +++ b/crates/ty_ide/src/signature_help.rs @@ -149,7 +149,7 @@ fn create_signature_details_from_call_signature_details( details .argument_to_parameter_mapping .get(current_arg_index) - .and_then(|¶m_index| param_index) + .and_then(|mapping| mapping.parameters.first().copied()) .or({ // If we can't find a mapping for this argument, but we have a current // argument index, use that as the active parameter if it's within bounds. @@ -242,11 +242,11 @@ fn find_active_signature_from_details(signature_details: &[CallSignatureDetails] // First, try to find a signature where all arguments have valid parameter mappings. let perfect_match = signature_details.iter().position(|details| { - // Check if all arguments have valid parameter mappings (i.e., are not None). + // Check if all arguments have valid parameter mappings. details .argument_to_parameter_mapping .iter() - .all(Option::is_some) + .all(|mapping| mapping.matched) }); if let Some(index) = perfect_match { @@ -261,7 +261,7 @@ fn find_active_signature_from_details(signature_details: &[CallSignatureDetails] details .argument_to_parameter_mapping .iter() - .filter(|mapping| mapping.is_some()) + .filter(|mapping| mapping.matched) .count() })?; diff --git a/crates/ty_python_semantic/resources/mdtest/call/function.md b/crates/ty_python_semantic/resources/mdtest/call/function.md index 847377ae00..30d636bede 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/function.md +++ b/crates/ty_python_semantic/resources/mdtest/call/function.md @@ -69,6 +69,246 @@ def _(flag: bool): reveal_type(foo()) # revealed: int ``` +## Splatted arguments + +### Unknown argument length + +```py +def takes_zero() -> None: ... +def takes_one(x: int) -> None: ... +def takes_two(x: int, y: int) -> None: ... +def takes_two_positional_only(x: int, y: int, /) -> None: ... +def takes_two_different(x: int, y: str) -> None: ... +def takes_two_different_positional_only(x: int, y: str, /) -> None: ... +def takes_at_least_zero(*args) -> None: ... +def takes_at_least_one(x: int, *args) -> None: ... +def takes_at_least_two(x: int, y: int, *args) -> None: ... +def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... + +# Test all of the above with a number of different splatted argument types + +def _(args: list[int]) -> None: + takes_zero(*args) + takes_one(*args) + takes_two(*args) + takes_two_positional_only(*args) + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) + +def _(args: tuple[int, ...]) -> None: + takes_zero(*args) + takes_one(*args) + takes_two(*args) + takes_two_positional_only(*args) + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) +``` + +### Fixed-length tuple argument + +```py +def takes_zero() -> None: ... +def takes_one(x: int) -> None: ... +def takes_two(x: int, y: int) -> None: ... +def takes_two_positional_only(x: int, y: int, /) -> None: ... +def takes_two_different(x: int, y: str) -> None: ... +def takes_two_different_positional_only(x: int, y: str, /) -> None: ... +def takes_at_least_zero(*args) -> None: ... +def takes_at_least_one(x: int, *args) -> None: ... +def takes_at_least_two(x: int, y: int, *args) -> None: ... +def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... + +# Test all of the above with a number of different splatted argument types + +def _(args: tuple[int]) -> None: + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) + takes_two(*args) # error: [missing-argument] + takes_two_positional_only(*args) # error: [missing-argument] + takes_two_different(*args) # error: [missing-argument] + takes_two_different_positional_only(*args) # error: [missing-argument] + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) # error: [missing-argument] + takes_at_least_two_positional_only(*args) # error: [missing-argument] + +def _(args: tuple[int, int]) -> None: + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] + takes_two(*args) + takes_two_positional_only(*args) + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) + +def _(args: tuple[int, str]) -> None: + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] + takes_two(*args) # error: [invalid-argument-type] + takes_two_positional_only(*args) # error: [invalid-argument-type] + takes_two_different(*args) + takes_two_different_positional_only(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) # error: [invalid-argument-type] + takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] +``` + +### Mixed tuple argument + +```toml +[environment] +python-version = "3.11" +``` + +```py +def takes_zero() -> None: ... +def takes_one(x: int) -> None: ... +def takes_two(x: int, y: int) -> None: ... +def takes_two_positional_only(x: int, y: int, /) -> None: ... +def takes_two_different(x: int, y: str) -> None: ... +def takes_two_different_positional_only(x: int, y: str, /) -> None: ... +def takes_at_least_zero(*args) -> None: ... +def takes_at_least_one(x: int, *args) -> None: ... +def takes_at_least_two(x: int, y: int, *args) -> None: ... +def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ... + +# Test all of the above with a number of different splatted argument types + +def _(args: tuple[int, *tuple[int, ...]]) -> None: + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) + takes_two(*args) + takes_two_positional_only(*args) + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) + +def _(args: tuple[int, *tuple[str, ...]]) -> None: + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) + takes_two(*args) # error: [invalid-argument-type] + takes_two_positional_only(*args) # error: [invalid-argument-type] + takes_two_different(*args) + takes_two_different_positional_only(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) # error: [invalid-argument-type] + takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] + +def _(args: tuple[int, int, *tuple[int, ...]]) -> None: + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] + takes_two(*args) + takes_two_positional_only(*args) + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) + +def _(args: tuple[int, int, *tuple[str, ...]]) -> None: + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] + takes_two(*args) + takes_two_positional_only(*args) + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) + +def _(args: tuple[int, *tuple[int, ...], int]) -> None: + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] + takes_two(*args) + takes_two_positional_only(*args) + takes_two_different(*args) # error: [invalid-argument-type] + takes_two_different_positional_only(*args) # error: [invalid-argument-type] + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) + takes_at_least_two_positional_only(*args) + +def _(args: tuple[int, *tuple[str, ...], int]) -> None: + takes_zero(*args) # error: [too-many-positional-arguments] + takes_one(*args) # error: [too-many-positional-arguments] + takes_two(*args) # error: [invalid-argument-type] + takes_two_positional_only(*args) # error: [invalid-argument-type] + takes_two_different(*args) + takes_two_different_positional_only(*args) + takes_at_least_zero(*args) + takes_at_least_one(*args) + takes_at_least_two(*args) # error: [invalid-argument-type] + takes_at_least_two_positional_only(*args) # error: [invalid-argument-type] +``` + +### Argument expansion regression + +This is a regression that was highlighted by the ecosystem check, which shows that we might need to +rethink how we perform argument expansion during overload resolution. In particular, we might need +to retry both `match_parameters` _and_ `check_types` for each expansion. Currently we only retry +`check_types`. + +The issue is that argument expansion might produce a splatted value with a different arity than what +we originally inferred for the unexpanded value, and that in turn can affect which parameters the +splatted value is matched with. + +The first example correctly produces an error. The `tuple[int, str]` union element has a precise +arity of two, and so parameter matching chooses the first overload. The second element of the tuple +does not match the second parameter type, which yielding an `invalid-argument-type` error. + +The third example should produce the same error. However, because we have a union, we do not see the +precise arity of each union element during parameter matching. Instead, we infer an arity of "zero +or more" for the union as a whole, and use that less precise arity when matching parameters. We +therefore consider the second overload to still be a potential candidate for the `tuple[int, str]` +union element. During type checking, we have to force the arity of each union element to match the +inferred arity of the union as a whole (turning `tuple[int, str]` into `tuple[int | str, ...]`). +That less precise tuple type-checks successfully against the second overload, making us incorrectly +think that `tuple[int, str]` is a valid splatted call. + +If we update argument expansion to retry parameter matching with the precise arity of each union +element, we will correctly rule out the second overload for `tuple[int, str]`, just like we do when +splatting that tuple directly (instead of as part of a union). + +```py +from typing import overload + +@overload +def f(x: int, y: int) -> None: ... +@overload +def f(x: int, y: str, z: int) -> None: ... +def f(*args): ... + +# Test all of the above with a number of different splatted argument types + +def _(t: tuple[int, str]) -> None: + f(*t) # error: [invalid-argument-type] + +def _(t: tuple[int, str, int]) -> None: + f(*t) + +def _(t: tuple[int, str] | tuple[int, str, int]) -> None: + # TODO: error: [invalid-argument-type] + f(*t) +``` + ## Wrong argument type ### Positional argument, positional-or-keyword parameter diff --git a/crates/ty_python_semantic/resources/mdtest/call/overloads.md b/crates/ty_python_semantic/resources/mdtest/call/overloads.md index 5ab5fb3360..8d1f2930cd 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/call/overloads.md @@ -5,6 +5,12 @@ with one or more overloads. This document describes the algorithm that it uses f matching, which is the same as the one mentioned in the [spec](https://typing.python.org/en/latest/spec/overload.html#overload-call-evaluation). +Note that all of the examples that involve positional parameters are tested multiple times: once +with the parameters matched with individual positional arguments, and once with the parameters +matched with a single positional argument that is splatted into the argument list. Overload +resolution is performed _after_ splatted arguments have been expanded, and so both approaches (TODO: +should) produce the same results. + ## Arity check The first step is to perform arity check. The non-overloaded cases are described in the @@ -26,10 +32,15 @@ from overloaded import f # These match a single overload reveal_type(f()) # revealed: None +reveal_type(f(*())) # revealed: None + reveal_type(f(1)) # revealed: int +reveal_type(f(*(1,))) # revealed: int # error: [no-matching-overload] "No overload of function `f` matches arguments" reveal_type(f("a", "b")) # revealed: Unknown +# error: [no-matching-overload] "No overload of function `f` matches arguments" +reveal_type(f(*("a", "b"))) # revealed: Unknown ``` ## Type checking @@ -59,8 +70,13 @@ which filters out all but the matching overload: from overloaded import f reveal_type(f(1)) # revealed: int +reveal_type(f(*(1,))) # revealed: int + reveal_type(f("a")) # revealed: str +reveal_type(f(*("a",))) # revealed: str + reveal_type(f(b"b")) # revealed: bytes +reveal_type(f(*(b"b",))) # revealed: bytes ``` ### Single match error @@ -88,9 +104,12 @@ from typing_extensions import reveal_type from overloaded import f reveal_type(f()) # revealed: None +reveal_type(f(*())) # revealed: None # error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["a"]`" reveal_type(f("a")) # revealed: Unknown +# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["a"]`" +reveal_type(f(*("a",))) # revealed: Unknown ``` More examples of this diagnostic can be found in the @@ -117,10 +136,15 @@ from overloaded import A, B, f # These calls pass the arity check, and type checking matches both overloads: reveal_type(f(A())) # revealed: A +reveal_type(f(*(A(),))) # revealed: A + reveal_type(f(B())) # revealed: A +# TODO: revealed: A +reveal_type(f(*(B(),))) # revealed: Unknown # But, in this case, the arity check filters out the first overload, so we only have one match: reveal_type(f(B(), 1)) # revealed: B +reveal_type(f(*(B(), 1))) # revealed: B ``` ## Argument type expansion @@ -155,8 +179,13 @@ from overloaded import A, B, C, f def _(ab: A | B, ac: A | C, bc: B | C): reveal_type(f(ab)) # revealed: A | B + reveal_type(f(*(ab,))) # revealed: A | B + reveal_type(f(bc)) # revealed: B | C + reveal_type(f(*(bc,))) # revealed: B | C + reveal_type(f(ac)) # revealed: A | C + reveal_type(f(*(ac,))) # revealed: A | C ``` ### Expanding first argument @@ -189,11 +218,15 @@ from overloaded import A, B, C, D, f def _(a_b: A | B): reveal_type(f(a_b, C())) # revealed: A | C + reveal_type(f(*(a_b, C()))) # revealed: A | C + reveal_type(f(a_b, D())) # revealed: B | D + reveal_type(f(*(a_b, D()))) # revealed: B | D # But, if it doesn't, it should expand the second argument and try again: def _(a_b: A | B, c_d: C | D): reveal_type(f(a_b, c_d)) # revealed: A | B | C | D + reveal_type(f(*(a_b, c_d))) # revealed: A | B | C | D ``` ### Expanding second argument @@ -226,9 +259,12 @@ def _(a: A, bc: B | C, cd: C | D): # This also tests that partial matching works correctly as the argument type expansion results # in matching the first and second overloads, but not the third one. reveal_type(f(a, bc)) # revealed: B | C + reveal_type(f(*(a, bc))) # revealed: B | C # error: [no-matching-overload] "No overload of function `f` matches arguments" reveal_type(f(a, cd)) # revealed: Unknown + # error: [no-matching-overload] "No overload of function `f` matches arguments" + reveal_type(f(*(a, cd))) # revealed: Unknown ``` ### Generics (legacy) @@ -254,7 +290,16 @@ from overloaded import A, f def _(x: int, y: A | int): reveal_type(f(x)) # revealed: int + # TODO: revealed: int + # TODO: no error + # error: [no-matching-overload] + reveal_type(f(*(x,))) # revealed: Unknown + reveal_type(f(y)) # revealed: A | int + # TODO: revealed: A | int + # TODO: no error + # error: [no-matching-overload] + reveal_type(f(*(y,))) # revealed: Unknown ``` ### Generics (PEP 695) @@ -283,7 +328,16 @@ from overloaded import B, f def _(x: int, y: B | int): reveal_type(f(x)) # revealed: int + # TODO: revealed: int + # TODO: no error + # error: [no-matching-overload] + reveal_type(f(*(x,))) # revealed: Unknown + reveal_type(f(y)) # revealed: B | int + # TODO: revealed: B | int + # TODO: no error + # error: [no-matching-overload] + reveal_type(f(*(y,))) # revealed: Unknown ``` ### Expanding `bool` @@ -307,8 +361,13 @@ from overloaded import f def _(flag: bool): reveal_type(f(True)) # revealed: T + reveal_type(f(*(True,))) # revealed: T + reveal_type(f(False)) # revealed: F + reveal_type(f(*(False,))) # revealed: F + reveal_type(f(flag)) # revealed: T | F + reveal_type(f(*(flag,))) # revealed: T | F ``` ### Expanding `tuple` @@ -338,6 +397,7 @@ from overloaded import A, B, f def _(x: tuple[A | B, int], y: tuple[int, bool]): reveal_type(f(x, y)) # revealed: A | B | C | D + reveal_type(f(*(x, y))) # revealed: A | B | C | D ``` ### Expanding `type` @@ -365,6 +425,7 @@ from overloaded import A, B, f def _(x: type[A | B]): reveal_type(x) # revealed: type[A] | type[B] reveal_type(f(x)) # revealed: A | B + reveal_type(f(*(x,))) # revealed: A | B ``` ### Expanding enums @@ -401,10 +462,19 @@ from overloaded import SomeEnum, A, B, C, f def _(x: SomeEnum, y: Literal[SomeEnum.A, SomeEnum.C]): reveal_type(f(SomeEnum.A)) # revealed: A + reveal_type(f(*(SomeEnum.A,))) # revealed: A + reveal_type(f(SomeEnum.B)) # revealed: B + reveal_type(f(*(SomeEnum.B,))) # revealed: B + reveal_type(f(SomeEnum.C)) # revealed: C + reveal_type(f(*(SomeEnum.C,))) # revealed: C + reveal_type(f(x)) # revealed: A | B | C + reveal_type(f(*(x,))) # revealed: A | B | C + reveal_type(f(y)) # revealed: A | C + reveal_type(f(*(y,))) # revealed: A | C ``` #### Enum with single member @@ -459,7 +529,7 @@ def _(missing: Literal[Missing.Value], missing_or_present: Literal[Missing.Value #### Enum subclass without members -An `Enum` subclass without members should *not* be expanded: +An `Enum` subclass without members should _not_ be expanded: `overloaded.pyi`: @@ -493,9 +563,19 @@ from overloaded import MyEnumSubclass, ActualEnum, f def _(actual_enum: ActualEnum, my_enum_instance: MyEnumSubclass): reveal_type(f(actual_enum)) # revealed: Both + # TODO: revealed: Both + reveal_type(f(*(actual_enum,))) # revealed: Unknown + reveal_type(f(ActualEnum.A)) # revealed: OnlyA + # TODO: revealed: OnlyA + reveal_type(f(*(ActualEnum.A,))) # revealed: Unknown + reveal_type(f(ActualEnum.B)) # revealed: OnlyB + # TODO: revealed: OnlyB + reveal_type(f(*(ActualEnum.B,))) # revealed: Unknown + reveal_type(f(my_enum_instance)) # revealed: MyEnumSubclass + reveal_type(f(*(my_enum_instance,))) # revealed: MyEnumSubclass ``` ### No matching overloads @@ -524,21 +604,22 @@ from overloaded import A, B, C, D, f def _(ab: A | B, ac: A | C, cd: C | D): reveal_type(f(ab)) # revealed: A | B + reveal_type(f(*(ab,))) # revealed: A | B # The `[A | C]` argument list is expanded to `[A], [C]` where the first list matches the first # overload while the second list doesn't match any of the overloads, so we generate an # error: [no-matching-overload] "No overload of function `f` matches arguments" reveal_type(f(ac)) # revealed: Unknown + # error: [no-matching-overload] "No overload of function `f` matches arguments" + reveal_type(f(*(ac,))) # revealed: Unknown # None of the expanded argument lists (`[C], [D]`) match any of the overloads, so we generate an # error: [no-matching-overload] "No overload of function `f` matches arguments" reveal_type(f(cd)) # revealed: Unknown + # error: [no-matching-overload] "No overload of function `f` matches arguments" + reveal_type(f(*(cd,))) # revealed: Unknown ``` -## Filtering overloads with variadic arguments and parameters - -TODO - ## Filtering based on `Any` / `Unknown` This is the step 5 of the overload call evaluation algorithm which specifies that: @@ -573,10 +654,16 @@ from overloaded import f # Anything other than `list` should match the last overload reveal_type(f(1)) # revealed: str +reveal_type(f(*(1,))) # revealed: str def _(list_int: list[int], list_any: list[Any]): reveal_type(f(list_int)) # revealed: int + # TODO: revealed: int + reveal_type(f(*(list_int,))) # revealed: Unknown + reveal_type(f(list_any)) # revealed: int + # TODO: revealed: int + reveal_type(f(*(list_any,))) # revealed: Unknown ``` ### Single list argument (ambiguous) @@ -604,16 +691,20 @@ from overloaded import f # Anything other than `list` should match the last overload reveal_type(f(1)) # revealed: str +reveal_type(f(*(1,))) # revealed: str def _(list_int: list[int], list_any: list[Any]): # All materializations of `list[int]` are assignable to `list[int]`, so it matches the first # overload. reveal_type(f(list_int)) # revealed: int + # TODO: revealed: int + reveal_type(f(*(list_int,))) # revealed: Unknown # All materializations of `list[Any]` are assignable to `list[int]` and `list[Any]`, but the # return type of first and second overloads are not equivalent, so the overload matching # is ambiguous. reveal_type(f(list_any)) # revealed: Unknown + reveal_type(f(*(list_any,))) # revealed: Unknown ``` ### Single tuple argument @@ -637,21 +728,33 @@ from typing import Any from overloaded import f reveal_type(f("a")) # revealed: str +reveal_type(f(*("a",))) # revealed: str + reveal_type(f((1, "b"))) # revealed: int +# TODO: revealed: int +reveal_type(f(*((1, "b"),))) # revealed: Unknown + reveal_type(f((1, 2))) # revealed: int +# TODO: revealed: int +reveal_type(f(*((1, 2),))) # revealed: Unknown def _(int_str: tuple[int, str], int_any: tuple[int, Any], any_any: tuple[Any, Any]): # All materializations are assignable to first overload, so second and third overloads are # eliminated reveal_type(f(int_str)) # revealed: int + # TODO: revealed: int + reveal_type(f(*(int_str,))) # revealed: Unknown # All materializations are assignable to second overload, so the third overload is eliminated; # the return type of first and second overload is equivalent reveal_type(f(int_any)) # revealed: int + # TODO: revealed: int + reveal_type(f(*(int_any,))) # revealed: Unknown # All materializations of `tuple[Any, Any]` are assignable to the parameters of all the # overloads, but the return types aren't equivalent, so the overload matching is ambiguous reveal_type(f(any_any)) # revealed: Unknown + reveal_type(f(*(any_any,))) # revealed: Unknown ``` ### Multiple arguments @@ -681,23 +784,32 @@ def _(list_int: list[int], list_any: list[Any], int_str: tuple[int, str], int_an # All materializations of both argument types are assignable to the first overload, so the # second and third overloads are filtered out reveal_type(f(list_int, int_str)) # revealed: A + # TODO: revealed: A + reveal_type(f(*(list_int, int_str))) # revealed: Unknown # All materialization of first argument is assignable to first overload and for the second # argument, they're assignable to the second overload, so the third overload is filtered out reveal_type(f(list_int, int_any)) # revealed: A + # TODO: revealed: A + reveal_type(f(*(list_int, int_any))) # revealed: Unknown # All materialization of first argument is assignable to second overload and for the second # argument, they're assignable to the first overload, so the third overload is filtered out reveal_type(f(list_any, int_str)) # revealed: A + # TODO: revealed: A + reveal_type(f(*(list_any, int_str))) # revealed: Unknown # All materializations of both arguments are assignable to the second overload, so the third # overload is filtered out reveal_type(f(list_any, int_any)) # revealed: A + # TODO: revealed: A + reveal_type(f(*(list_any, int_any))) # revealed: Unknown # All materializations of first argument is assignable to the second overload and for the second # argument, they're assignable to the third overload, so no overloads are filtered out; the # return types of the remaining overloads are not equivalent, so overload matching is ambiguous reveal_type(f(list_int, any_any)) # revealed: Unknown + reveal_type(f(*(list_int, any_any))) # revealed: Unknown ``` ### `LiteralString` and `str` @@ -722,11 +834,16 @@ from overloaded import f def _(literal: LiteralString, string: str, any: Any): reveal_type(f(literal)) # revealed: LiteralString + # TODO: revealed: LiteralString + reveal_type(f(*(literal,))) # revealed: Unknown + reveal_type(f(string)) # revealed: str + reveal_type(f(*(string,))) # revealed: str # `Any` matches both overloads, but the return types are not equivalent. # Pyright and mypy both reveal `str` here, contrary to the spec. reveal_type(f(any)) # revealed: Unknown + reveal_type(f(*(any,))) # revealed: Unknown ``` ### Generics @@ -756,10 +873,19 @@ from overloaded import f def _(list_int: list[int], list_str: list[str], list_any: list[Any], any: Any): reveal_type(f(list_int)) # revealed: A + # TODO: revealed: A + reveal_type(f(*(list_int,))) # revealed: Unknown + # TODO: Should be `str` reveal_type(f(list_str)) # revealed: Unknown + # TODO: Should be `str` + reveal_type(f(*(list_str,))) # revealed: Unknown + reveal_type(f(list_any)) # revealed: Unknown + reveal_type(f(*(list_any,))) # revealed: Unknown + reveal_type(f(any)) # revealed: Unknown + reveal_type(f(*(any,))) # revealed: Unknown ``` ### Generics (multiple arguments) @@ -784,12 +910,24 @@ from overloaded import f def _(integer: int, string: str, any: Any, list_any: list[Any]): reveal_type(f(integer, string)) # revealed: int + reveal_type(f(*(integer, string))) # revealed: int + reveal_type(f(string, integer)) # revealed: int + # TODO: revealed: int + # TODO: no error + # error: [no-matching-overload] + reveal_type(f(*(string, integer))) # revealed: Unknown # This matches the second overload and is _not_ the case of ambiguous overload matching. reveal_type(f(string, any)) # revealed: Any + # TODO: Any + reveal_type(f(*(string, any))) # revealed: tuple[str, Any] reveal_type(f(string, list_any)) # revealed: list[Any] + # TODO: revealed: list[Any] + # TODO: no error + # error: [no-matching-overload] + reveal_type(f(*(string, list_any))) # revealed: Unknown ``` ### Generic `self` @@ -832,7 +970,54 @@ def _(b_int: B[int], b_str: B[str], b_any: B[Any]): ### Variadic argument -TODO: A variadic parameter is being assigned to a number of parameters of the same type +`overloaded.pyi`: + +```pyi +from typing import Any, overload + +class A: ... +class B: ... + +@overload +def f1(x: int) -> A: ... +@overload +def f1(x: Any, y: Any) -> A: ... + +@overload +def f2(x: int) -> A: ... +@overload +def f2(x: Any, y: Any) -> B: ... + +@overload +def f3(x: int) -> A: ... +@overload +def f3(x: Any, y: Any) -> A: ... +@overload +def f3(x: Any, y: Any, *, z: str) -> B: ... + +@overload +def f4(x: int) -> A: ... +@overload +def f4(x: Any, y: Any) -> B: ... +@overload +def f4(x: Any, y: Any, *, z: str) -> B: ... +``` + +```py +from typing import Any + +from overloaded import f1, f2, f3, f4 + +def _(arg: list[Any]): + # Matches both overload and the return types are equivalent + reveal_type(f1(*arg)) # revealed: A + # Matches both overload but the return types aren't equivalent + reveal_type(f2(*arg)) # revealed: Unknown + # Filters out the final overload and the return types are equivalent + reveal_type(f3(*arg)) # revealed: A + # Filters out the final overload but the return types aren't equivalent + reveal_type(f4(*arg)) # revealed: Unknown +``` ### Non-participating fully-static parameter @@ -868,7 +1053,10 @@ from overloaded import f def _(any: Any): reveal_type(f(any, flag=True)) # revealed: int + reveal_type(f(*(any,), flag=True)) # revealed: int + reveal_type(f(any, flag=False)) # revealed: str + reveal_type(f(*(any,), flag=False)) # revealed: str ``` ### Non-participating gradual parameter @@ -879,21 +1067,32 @@ def _(any: Any): from typing import Any, Literal, overload @overload -def f(x: tuple[str, Any], *, flag: Literal[True]) -> int: ... +def f(x: tuple[str, Any], flag: Literal[True]) -> int: ... @overload -def f(x: tuple[str, Any], *, flag: Literal[False] = ...) -> str: ... +def f(x: tuple[str, Any], flag: Literal[False] = ...) -> str: ... @overload -def f(x: tuple[str, Any], *, flag: bool = ...) -> int | str: ... +def f(x: tuple[str, Any], flag: bool = ...) -> int | str: ... ``` ```py -from typing import Any +from typing import Any, Literal from overloaded import f def _(any: Any): reveal_type(f(any, flag=True)) # revealed: int + reveal_type(f(*(any,), flag=True)) # revealed: int + reveal_type(f(any, flag=False)) # revealed: str + reveal_type(f(*(any,), flag=False)) # revealed: str + +def _(args: tuple[Any, Literal[True]]): + # TODO: revealed: int + reveal_type(f(*args)) # revealed: Unknown + +def _(args: tuple[Any, Literal[False]]): + # TODO: revealed: str + reveal_type(f(*args)) # revealed: Unknown ``` ### Argument type expansion @@ -938,6 +1137,7 @@ from overloaded import A, B, f def _(arg: tuple[A | B, Any]): reveal_type(f(arg)) # revealed: A | B + reveal_type(f(*(arg,))) # revealed: A | B ``` #### One argument list ambiguous @@ -972,6 +1172,7 @@ from overloaded import A, B, C, f def _(arg: tuple[A | B, Any]): reveal_type(f(arg)) # revealed: A | Unknown + reveal_type(f(*(arg,))) # revealed: A | Unknown ``` #### Both argument lists ambiguous @@ -1005,4 +1206,5 @@ from overloaded import A, B, C, f def _(arg: tuple[A | B, Any]): reveal_type(f(arg)) # revealed: Unknown + reveal_type(f(*(arg,))) # revealed: Unknown ``` diff --git a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md index 69586641c9..5d9bcda7c0 100644 --- a/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md +++ b/crates/ty_python_semantic/resources/mdtest/narrow/type_guards.md @@ -161,8 +161,7 @@ def _(d: Any): if f(): # error: [missing-argument] ... - # TODO: no error, once we support splatted call args - if g(*d): # error: [missing-argument] + if g(*d): ... if f("foo"): # TODO: error: [invalid-type-guard-call] diff --git a/crates/ty_python_semantic/src/types/call.rs b/crates/ty_python_semantic/src/types/call.rs index c34489bbbc..be85b3922a 100644 --- a/crates/ty_python_semantic/src/types/call.rs +++ b/crates/ty_python_semantic/src/types/call.rs @@ -5,7 +5,7 @@ use crate::Db; mod arguments; pub(crate) mod bind; pub(super) use arguments::{Argument, CallArguments}; -pub(super) use bind::{Binding, Bindings, CallableBinding}; +pub(super) use bind::{Binding, Bindings, CallableBinding, MatchedArgument}; /// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was /// unsuccessful. diff --git a/crates/ty_python_semantic/src/types/call/arguments.rs b/crates/ty_python_semantic/src/types/call/arguments.rs index c463d0a982..497c412d5c 100644 --- a/crates/ty_python_semantic/src/types/call/arguments.rs +++ b/crates/ty_python_semantic/src/types/call/arguments.rs @@ -6,7 +6,7 @@ use ruff_python_ast as ast; use crate::Db; use crate::types::KnownClass; use crate::types::enums::enum_member_literals; -use crate::types::tuple::{TupleSpec, TupleType}; +use crate::types::tuple::{TupleLength, TupleSpec, TupleType}; use super::Type; @@ -16,8 +16,8 @@ pub(crate) enum Argument<'a> { Synthetic, /// A positional argument. Positional, - /// A starred positional argument (e.g. `*args`). - Variadic, + /// A starred positional argument (e.g. `*args`) containing the specified number of elements. + Variadic(TupleLength), /// A keyword argument (e.g. `a=1`). Keyword(&'a str), /// The double-starred keywords argument (e.g. `**kwargs`). @@ -37,24 +37,38 @@ impl<'a, 'db> CallArguments<'a, 'db> { Self { arguments, types } } - /// Create `CallArguments` from AST arguments - pub(crate) fn from_arguments(arguments: &'a ast::Arguments) -> Self { + /// Create `CallArguments` from AST arguments. We will use the provided callback to obtain the + /// type of each splatted argument, so that we can determine its length. All other arguments + /// will remain uninitialized as `Unknown`. + pub(crate) fn from_arguments( + db: &'db dyn Db, + arguments: &'a ast::Arguments, + mut infer_argument_type: impl FnMut(&ast::Expr, &ast::Expr) -> Type<'db>, + ) -> Self { arguments .arguments_source_order() .map(|arg_or_keyword| match arg_or_keyword { ast::ArgOrKeyword::Arg(arg) => match arg { - ast::Expr::Starred(ast::ExprStarred { .. }) => Argument::Variadic, - _ => Argument::Positional, + ast::Expr::Starred(ast::ExprStarred { value, .. }) => { + let ty = infer_argument_type(arg, value); + let length = match ty { + Type::Tuple(tuple) => tuple.tuple(db).len(), + // TODO: have `Type::try_iterator` return a tuple spec, and use its + // length as this argument's arity + _ => TupleLength::unknown(), + }; + (Argument::Variadic(length), Some(ty)) + } + _ => (Argument::Positional, None), }, ast::ArgOrKeyword::Keyword(ast::Keyword { arg, .. }) => { if let Some(arg) = arg { - Argument::Keyword(&arg.id) + (Argument::Keyword(&arg.id), None) } else { - Argument::Keywords + (Argument::Keywords, None) } } }) - .map(|argument| (argument, None)) .collect() } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 3bbda82594..3546013e0c 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -3,6 +3,7 @@ //! [signatures][crate::types::signatures], we have to handle the fact that the callable might be a //! union of types, each of which might contain multiple overloads. +use std::borrow::Cow; use std::collections::HashSet; use std::fmt; @@ -25,7 +26,7 @@ use crate::types::function::{ }; use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError}; use crate::types::signatures::{Parameter, ParameterForm, Parameters}; -use crate::types::tuple::TupleType; +use crate::types::tuple::{Tuple, TupleLength, TupleType}; use crate::types::{ BoundMethodType, ClassLiteral, DataclassParams, KnownClass, KnownInstanceType, MethodWrapperKind, PropertyInstanceType, SpecialFormType, TypeMapping, UnionType, @@ -1388,24 +1389,22 @@ impl<'db> CallableBinding<'db> { let mut first_parameter_type: Option> = None; let mut participating_parameter_index = None; - for overload_index in matching_overload_indexes { + 'overload: for overload_index in matching_overload_indexes { let overload = &self.overloads[*overload_index]; - let Some(parameter_index) = overload.argument_parameters[argument_index] else { - // There is no parameter for this argument in this overload. - break; - }; - // TODO: For an unannotated `self` / `cls` parameter, the type should be - // `typing.Self` / `type[typing.Self]` - let current_parameter_type = overload.signature.parameters()[parameter_index] - .annotated_type() - .unwrap_or(Type::unknown()); - if let Some(first_parameter_type) = first_parameter_type { - if !first_parameter_type.is_equivalent_to(db, current_parameter_type) { - participating_parameter_index = Some(parameter_index); - break; + for parameter_index in &overload.argument_matches[argument_index].parameters { + // TODO: For an unannotated `self` / `cls` parameter, the type should be + // `typing.Self` / `type[typing.Self]` + let current_parameter_type = overload.signature.parameters()[*parameter_index] + .annotated_type() + .unwrap_or(Type::unknown()); + if let Some(first_parameter_type) = first_parameter_type { + if !first_parameter_type.is_equivalent_to(db, current_parameter_type) { + participating_parameter_index = Some(*parameter_index); + break 'overload; + } + } else { + first_parameter_type = Some(current_parameter_type); } - } else { - first_parameter_type = Some(current_parameter_type); } } @@ -1433,20 +1432,18 @@ impl<'db> CallableBinding<'db> { let mut current_parameter_types = vec![]; for overload_index in &matching_overload_indexes[..=upto] { let overload = &self.overloads[*overload_index]; - let Some(parameter_index) = overload.argument_parameters[argument_index] else { - // There is no parameter for this argument in this overload. - continue; - }; - if !participating_parameter_indexes.contains(¶meter_index) { - // This parameter doesn't participate in the filtering process. - continue; + for parameter_index in &overload.argument_matches[argument_index].parameters { + if !participating_parameter_indexes.contains(parameter_index) { + // This parameter doesn't participate in the filtering process. + continue; + } + // TODO: For an unannotated `self` / `cls` parameter, the type should be + // `typing.Self` / `type[typing.Self]` + let parameter_type = overload.signature.parameters()[*parameter_index] + .annotated_type() + .unwrap_or(Type::unknown()); + current_parameter_types.push(parameter_type); } - // TODO: For an unannotated `self` / `cls` parameter, the type should be - // `typing.Self` / `type[typing.Self]` - let parameter_type = overload.signature.parameters()[parameter_index] - .annotated_type() - .unwrap_or(Type::unknown()); - current_parameter_types.push(parameter_type); } if current_parameter_types.is_empty() { continue; @@ -1761,9 +1758,7 @@ struct ArgumentMatcher<'a, 'db> { conflicting_forms: &'a mut [bool], errors: &'a mut Vec>, - /// The parameter that each argument is matched with. - argument_parameters: Vec>, - /// Whether each parameter has been matched with an argument. + argument_matches: Vec, parameter_matched: Vec, next_positional: usize, first_excess_positional: Option, @@ -1783,7 +1778,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { argument_forms, conflicting_forms, errors, - argument_parameters: vec![None; arguments.len()], + argument_matches: vec![MatchedArgument::default(); arguments.len()], parameter_matched: vec![false; parameters.len()], next_positional: 0, first_excess_positional: None, @@ -1828,7 +1823,9 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { }); } } - self.argument_parameters[argument_index] = Some(parameter_index); + let matched_argument = &mut self.argument_matches[argument_index]; + matched_argument.parameters.push(parameter_index); + matched_argument.matched = true; self.parameter_matched[parameter_index] = true; } @@ -1882,7 +1879,34 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { Ok(()) } - fn finish(self) -> Box<[Option]> { + fn match_variadic( + &mut self, + argument_index: usize, + argument: Argument<'a>, + length: TupleLength, + ) -> Result<(), ()> { + // We must be able to match up the fixed-length portion of the argument with positional + // parameters, so we pass on any errors that occur. + for _ in 0..length.minimum() { + self.match_positional(argument_index, argument)?; + } + + // If the tuple is variable-length, we assume that it will soak up all remaining positional + // parameters. + if length.is_variable() { + while self + .parameters + .get_positional(self.next_positional) + .is_some() + { + self.match_positional(argument_index, argument)?; + } + } + + Ok(()) + } + + fn finish(self) -> Box<[MatchedArgument]> { if let Some(first_excess_argument_index) = self.first_excess_positional { self.errors.push(BindingError::TooManyPositionalArguments { first_excess_argument_index: self.get_argument_index(first_excess_argument_index), @@ -1911,7 +1935,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { }); } - self.argument_parameters.into_boxed_slice() + self.argument_matches.into_boxed_slice() } } @@ -1919,7 +1943,7 @@ struct ArgumentTypeChecker<'a, 'db> { db: &'db dyn Db, signature: &'a Signature<'db>, arguments: &'a CallArguments<'a, 'db>, - argument_parameters: &'a [Option], + argument_matches: &'a [MatchedArgument], parameter_tys: &'a mut [Option>], errors: &'a mut Vec>, @@ -1932,7 +1956,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { db: &'db dyn Db, signature: &'a Signature<'db>, arguments: &'a CallArguments<'a, 'db>, - argument_parameters: &'a [Option], + argument_matches: &'a [MatchedArgument], parameter_tys: &'a mut [Option>], errors: &'a mut Vec>, ) -> Self { @@ -1940,7 +1964,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { db, signature, arguments, - argument_parameters, + argument_matches, parameter_tys, errors, specialization: None, @@ -1987,20 +2011,17 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for (argument_index, adjusted_argument_index, _, argument_type) in self.enumerate_argument_types() { - let Some(parameter_index) = self.argument_parameters[argument_index] else { - // There was an error with argument when matching parameters, so don't bother - // type-checking it. - continue; - }; - let parameter = ¶meters[parameter_index]; - let Some(expected_type) = parameter.annotated_type() else { - continue; - }; - if let Err(error) = builder.infer(expected_type, argument_type) { - self.errors.push(BindingError::SpecializationError { - error, - argument_index: adjusted_argument_index, - }); + for parameter_index in &self.argument_matches[argument_index].parameters { + let parameter = ¶meters[*parameter_index]; + let Some(expected_type) = parameter.annotated_type() else { + continue; + }; + if let Err(error) = builder.infer(expected_type, argument_type) { + self.errors.push(BindingError::SpecializationError { + error, + argument_index: adjusted_argument_index, + }); + } } } self.specialization = self.signature.generic_context.map(|gc| builder.build(gc)); @@ -2016,16 +2037,11 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { fn check_argument_type( &mut self, - argument_index: usize, adjusted_argument_index: Option, argument: Argument<'a>, mut argument_type: Type<'db>, + parameter_index: usize, ) { - let Some(parameter_index) = self.argument_parameters[argument_index] else { - // There was an error with argument when matching parameters, so don't bother - // type-checking it. - return; - }; let parameters = self.signature.parameters(); let parameter = ¶meters[parameter_index]; if let Some(mut expected_ty) = parameter.annotated_type() { @@ -2064,12 +2080,73 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { for (argument_index, adjusted_argument_index, argument, argument_type) in self.enumerate_argument_types() { - self.check_argument_type( - argument_index, - adjusted_argument_index, - argument, - argument_type, - ); + // If the argument isn't splatted, just check its type directly. + let Argument::Variadic(_) = argument else { + for parameter_index in &self.argument_matches[argument_index].parameters { + self.check_argument_type( + adjusted_argument_index, + argument, + argument_type, + *parameter_index, + ); + } + continue; + }; + + // If the argument is splatted, convert its type into a tuple describing the splatted + // elements. For tuples, we don't have to do anything! For other types, we treat it as + // an iterator, and create a homogeneous tuple of its output type, since we don't know + // how many elements the iterator will produce. + // TODO: update `Type::try_iterate` to return this tuple type for us. + let argument_types = match argument_type { + Type::Tuple(tuple) => Cow::Borrowed(tuple.tuple(self.db)), + _ => { + let element_type = argument_type.iterate(self.db); + Cow::Owned(Tuple::homogeneous(element_type)) + } + }; + + // TODO: When we perform argument expansion during overload resolution, we might need + // to retry both `match_parameters` _and_ `check_types` for each expansion. Currently + // we only retry `check_types`. The issue is that argument expansion might produce a + // splatted value with a different arity than what we originally inferred for the + // unexpanded value, and that in turn can affect which parameters the splatted value is + // matched with. As a workaround, make sure that the splatted tuple contains an + // arbitrary number of `Unknown`s at the end, so that if the expanded value has a + // smaller arity than the unexpanded value, we still have enough values to assign to + // the already matched parameters. + let argument_types = match argument_types.as_ref() { + Tuple::Fixed(_) => { + Cow::Owned(argument_types.concat(self.db, &Tuple::homogeneous(Type::unknown()))) + } + Tuple::Variable(_) => argument_types, + }; + + // Resize the tuple of argument types to line up with the number of parameters this + // argument was matched against. If parameter matching succeeded, then we can (TODO: + // should be able to, see above) guarantee that all of the required elements of the + // splatted tuple will have been matched with a parameter. But if parameter matching + // failed, there might be more required elements. That means we can't use + // TupleLength::Fixed below, because we would otherwise get a "too many values" error + // when parameter matching failed. + let desired_size = + TupleLength::Variable(self.argument_matches[argument_index].parameters.len(), 0); + let argument_types = argument_types + .resize(self.db, desired_size) + .expect("argument type should be consistent with its arity"); + + // Check the types by zipping through the splatted argument types and their matched + // parameters. + for (argument_type, parameter_index) in (argument_types.all_elements()) + .zip(&self.argument_matches[argument_index].parameters) + { + self.check_argument_type( + adjusted_argument_index, + argument, + *argument_type, + *parameter_index, + ); + } } } @@ -2078,6 +2155,20 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { } } +/// Information about which parameter(s) an argument was matched against. This is tracked +/// separately for each overload. +#[derive(Clone, Debug, Default)] +pub struct MatchedArgument { + /// The index of the parameter(s) that an argument was matched against. A splatted argument + /// might be matched against multiple parameters. + pub parameters: SmallVec<[usize; 1]>, + + /// Whether there were errors matching this argument. For a splatted argument, _all_ splatted + /// elements must have been successfully matched. (That means that this can be `false` while + /// the `parameters` field is non-empty.) + pub matched: bool, +} + /// Binding information for one of the overloads of a callable. #[derive(Debug)] pub(crate) struct Binding<'db> { @@ -2101,9 +2192,9 @@ pub(crate) struct Binding<'db> { /// is being used to infer a specialization for the class. inherited_specialization: Option>, - /// The formal parameter that each argument is matched with, in argument source order, or - /// `None` if the argument was not matched to any parameter. - argument_parameters: Box<[Option]>, + /// Information about which parameter(s) each argument was matched with, in argument source + /// order. + argument_matches: Box<[MatchedArgument]>, /// Bound types for parameters, in parameter source order, or `None` if no argument was matched /// to that parameter. @@ -2122,7 +2213,7 @@ impl<'db> Binding<'db> { return_ty: Type::unknown(), specialization: None, inherited_specialization: None, - argument_parameters: Box::from([]), + argument_matches: Box::from([]), parameter_tys: Box::from([]), errors: vec![], } @@ -2156,7 +2247,10 @@ impl<'db> Binding<'db> { Argument::Keyword(name) => { let _ = matcher.match_keyword(argument_index, argument, name); } - Argument::Variadic | Argument::Keywords => { + Argument::Variadic(length) => { + let _ = matcher.match_variadic(argument_index, argument, length); + } + Argument::Keywords => { // TODO continue; } @@ -2164,7 +2258,7 @@ impl<'db> Binding<'db> { } self.return_ty = self.signature.return_ty.unwrap_or(Type::unknown()); self.parameter_tys = vec![None; parameters.len()].into_boxed_slice(); - self.argument_parameters = matcher.finish(); + self.argument_matches = matcher.finish(); } fn check_types(&mut self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) { @@ -2172,7 +2266,7 @@ impl<'db> Binding<'db> { db, &self.signature, arguments, - &self.argument_parameters, + &self.argument_matches, &mut self.parameter_tys, &mut self.errors, ); @@ -2218,9 +2312,9 @@ impl<'db> Binding<'db> { ) -> impl Iterator, Type<'db>)> + 'a { argument_types .iter() - .zip(&self.argument_parameters) - .filter(move |(_, argument_parameter)| { - argument_parameter.is_some_and(|ap| ap == parameter_index) + .zip(&self.argument_matches) + .filter(move |(_, argument_matches)| { + argument_matches.parameters.contains(¶meter_index) }) .map(|((argument, argument_type), _)| { (argument, argument_type.unwrap_or_else(Type::unknown)) @@ -2265,7 +2359,7 @@ impl<'db> Binding<'db> { return_ty: self.return_ty, specialization: self.specialization, inherited_specialization: self.inherited_specialization, - argument_parameters: self.argument_parameters.clone(), + argument_matches: self.argument_matches.clone(), parameter_tys: self.parameter_tys.clone(), errors: self.errors.clone(), } @@ -2276,7 +2370,7 @@ impl<'db> Binding<'db> { return_ty, specialization, inherited_specialization, - argument_parameters, + argument_matches, parameter_tys, errors, } = snapshot; @@ -2284,15 +2378,15 @@ impl<'db> Binding<'db> { self.return_ty = return_ty; self.specialization = specialization; self.inherited_specialization = inherited_specialization; - self.argument_parameters = argument_parameters; + self.argument_matches = argument_matches; self.parameter_tys = parameter_tys; self.errors = errors; } /// Returns a vector where each index corresponds to an argument position, /// and the value is the parameter index that argument maps to (if any). - pub(crate) fn argument_to_parameter_mapping(&self) -> &[Option] { - &self.argument_parameters + pub(crate) fn argument_matches(&self) -> &[MatchedArgument] { + &self.argument_matches } } @@ -2301,7 +2395,7 @@ struct BindingSnapshot<'db> { return_ty: Type<'db>, specialization: Option>, inherited_specialization: Option>, - argument_parameters: Box<[Option]>, + argument_matches: Box<[MatchedArgument]>, parameter_tys: Box<[Option>]>, errors: Vec>, } @@ -2342,8 +2436,8 @@ impl<'db> CallableBindingSnapshot<'db> { snapshot.specialization = binding.specialization; snapshot.inherited_specialization = binding.inherited_specialization; snapshot - .argument_parameters - .clone_from(&binding.argument_parameters); + .argument_matches + .clone_from(&binding.argument_matches); snapshot.parameter_tys.clone_from(&binding.parameter_tys); } diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index 2ab52ef0fa..59fe54412c 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -9,7 +9,7 @@ use crate::semantic_index::place::ScopeId; use crate::semantic_index::{ attribute_scopes, global_scope, place_table, semantic_index, use_def_map, }; -use crate::types::call::CallArguments; +use crate::types::call::{CallArguments, MatchedArgument}; use crate::types::signatures::Signature; use crate::types::{ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstanceType, Type}; use crate::{Db, HasType, NameKind, SemanticModel}; @@ -724,8 +724,8 @@ pub struct CallSignatureDetails<'db> { pub definition: Option>, /// Mapping from argument indices to parameter indices. This helps - /// identify which argument corresponds to which parameter. - pub argument_to_parameter_mapping: Vec>, + /// determine which parameter corresponds to which argument position. + pub argument_to_parameter_mapping: Vec, } /// Extract signature details from a function call expression. @@ -742,7 +742,10 @@ pub fn call_signature_details<'db>( // Use into_callable to handle all the complex type conversions if let Some(callable_type) = func_type.into_callable(db) { - let call_arguments = CallArguments::from_arguments(&call_expr.arguments); + let call_arguments = + CallArguments::from_arguments(db, &call_expr.arguments, |_, splatted_value| { + splatted_value.inferred_type(&model) + }); let bindings = callable_type.bindings(db).match_parameters(&call_arguments); // Extract signature details from all callable bindings @@ -761,7 +764,7 @@ pub fn call_signature_details<'db>( parameter_label_offsets, parameter_names, definition: signature.definition(), - argument_to_parameter_mapping: binding.argument_to_parameter_mapping().to_vec(), + argument_to_parameter_mapping: binding.argument_matches().to_vec(), } }) .collect() diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index 0f9733fafc..0e00b9eecb 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -2203,7 +2203,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { self.infer_type_parameters(type_params); if let Some(arguments) = class.arguments.as_deref() { - let mut call_arguments = CallArguments::from_arguments(arguments); + let mut call_arguments = + CallArguments::from_arguments(self.db(), arguments, |argument, splatted_value| { + let ty = self.infer_expression(splatted_value); + self.store_expression_type(argument, ty); + ty + }); let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; self.infer_argument_types(arguments, &mut call_arguments, &argument_forms); } @@ -5165,19 +5170,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .zip(argument_forms.iter().copied()) .zip(ast_arguments.arguments_source_order()); for (((_, argument_type), form), arg_or_keyword) in iter { - let ty = match arg_or_keyword { - ast::ArgOrKeyword::Arg(arg) => match arg { - ast::Expr::Starred(ast::ExprStarred { value, .. }) => { - let ty = self.infer_argument_type(value, form); - self.store_expression_type(arg, ty); - ty - } - _ => self.infer_argument_type(arg, form), - }, - ast::ArgOrKeyword::Keyword(ast::Keyword { value, .. }) => { - self.infer_argument_type(value, form) - } + let argument = match arg_or_keyword { + // We already inferred the type of splatted arguments. + ast::ArgOrKeyword::Arg(ast::Expr::Starred(_)) => continue, + ast::ArgOrKeyword::Arg(arg) => arg, + ast::ArgOrKeyword::Keyword(ast::Keyword { value, .. }) => value, }; + let ty = self.infer_argument_type(argument, form); *argument_type = Some(ty); } } @@ -5874,7 +5873,12 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // We don't call `Type::try_call`, because we want to perform type inference on the // arguments after matching them to parameters, but before checking that the argument types // are assignable to any parameter annotations. - let mut call_arguments = CallArguments::from_arguments(arguments); + let mut call_arguments = + CallArguments::from_arguments(self.db(), arguments, |argument, splatted_value| { + let ty = self.infer_expression(splatted_value); + self.store_expression_type(argument, ty); + ty + }); let callable_type = self.infer_maybe_standalone_expression(func); diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index fe1ceb5aed..0075eddff1 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -38,6 +38,14 @@ pub(crate) enum TupleLength { } impl TupleLength { + pub(crate) const fn unknown() -> TupleLength { + TupleLength::Variable(0, 0) + } + + pub(crate) fn is_variable(self) -> bool { + matches!(self, TupleLength::Variable(_, _)) + } + /// Returns the minimum and maximum length of this tuple. (The maximum length will be `None` /// for a tuple with a variable-length portion.) pub(crate) fn size_hint(self) -> (usize, Option) {