Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6a3d6dd
add length to variadic argument
dcreager Jun 26, 2025
114a491
match variadic args
dcreager Jun 26, 2025
cf39e0d
resize to variable-length
dcreager Jun 27, 2025
c116820
infer correct arity for splatted tuples
dcreager Jun 27, 2025
953af0e
clippy
dcreager Jun 27, 2025
1949dfb
mdlint
dcreager Jun 27, 2025
4f4cf2b
fix those panics
dcreager Jun 27, 2025
0954aab
add comments
dcreager Jun 27, 2025
910bb1d
argument expansion workaround
dcreager Jun 27, 2025
e5bc935
mdlint
dcreager Jun 27, 2025
87f2ff9
refine comment
dcreager Jun 27, 2025
e8c476d
Combine CallArguments and CallArgumentTypes
dcreager Jul 14, 2025
3a57137
fix docs
dcreager Jul 14, 2025
dec9b73
Merge branch 'dcreager/merge-arguments' into dcreager/splat
dcreager Jul 14, 2025
8929733
wrap in option
dcreager Jul 14, 2025
988479d
move around a bit
dcreager Jul 14, 2025
4a13a6f
Merge branch 'dcreager/merge-arguments' into dcreager/splat
dcreager Jul 14, 2025
06f75c4
fix tests
dcreager Jul 14, 2025
40d117b
use FromIterator
dcreager Jul 15, 2025
5fdaed8
remove unused From
dcreager Jul 15, 2025
d389168
debug assert lengths
dcreager Jul 15, 2025
900240b
add asserting constructor
dcreager Jul 15, 2025
3a7c04d
add types iterator
dcreager Jul 15, 2025
9a1175c
Merge branch 'main' into dcreager/merge-arguments
dcreager Jul 15, 2025
0f0cd47
Merge branch 'dcreager/merge-arguments' into dcreager/splat
dcreager Jul 15, 2025
67a5f66
Merge branch 'main' into dcreager/splat
dcreager Jul 15, 2025
8b65f34
use type alias for arg/param map
dcreager Jul 15, 2025
043bcb6
better argument expansion regression test
dcreager Jul 15, 2025
57c9afc
add more arg type tests
dcreager Jul 15, 2025
d16dbbb
MatchedArgument
dcreager Jul 15, 2025
41447e3
add tests from dhruv
dcreager Jul 15, 2025
ec219b4
break out of the right loop
dcreager Jul 15, 2025
3290874
add positional-only tests
dcreager Jul 15, 2025
369ef05
mdlint
dcreager Jul 15, 2025
c9b9b48
Merge branch 'main' into dcreager/splat
dcreager Jul 22, 2025
abd0c9d
add overload equivalent of every test
dcreager Jul 22, 2025
b1b6087
mdlint
dcreager Jul 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/ty_ide/src/signature_help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ fn create_signature_details_from_call_signature_details(
details
.argument_to_parameter_mapping
.get(current_arg_index)
.and_then(|&param_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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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()
})?;

Expand Down
240 changes: 240 additions & 0 deletions crates/ty_python_semantic/resources/mdtest/call/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading