Skip to content

Commit 427872b

Browse files
charliermarshclaude
andcommitted
[ty] Add ParamSpec context to missing argument diagnostics
When a ParamSpec callable is called without the required `*args` or `**kwargs`, the error message now includes a sub-diagnostic explaining why these arguments are required. This addresses reviewer feedback from PR #22820 about making it clearer why these parameters are required in some cases and not in others. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent c62dad5 commit 427872b

3 files changed

Lines changed: 114 additions & 1 deletion

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Missing argument for ParamSpec
2+
3+
<!-- snapshot-diagnostics -->
4+
5+
For `ParamSpec` callables, both `*args` and `**kwargs` are required since the underlying callable's
6+
signature is unknown. We add a sub-diagnostic explaining why these parameters are required.
7+
8+
```toml
9+
[environment]
10+
python-version = "3.12"
11+
```
12+
13+
```py
14+
from typing import Callable
15+
16+
def decorator[**P](func: Callable[P, int]) -> Callable[P, None]:
17+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
18+
func() # error: [missing-argument]
19+
func(*args) # error: [missing-argument]
20+
func(**kwargs) # error: [missing-argument]
21+
return wrapper
22+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
source: crates/ty_test/src/lib.rs
3+
expression: snapshot
4+
---
5+
6+
---
7+
mdtest name: missing_argument_paramspec.md - Missing argument for ParamSpec
8+
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument_paramspec.md
9+
---
10+
11+
# Python source files
12+
13+
## mdtest_snippet.py
14+
15+
```
16+
1 | from typing import Callable
17+
2 |
18+
3 | def decorator[**P](func: Callable[P, int]) -> Callable[P, None]:
19+
4 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
20+
5 | func() # error: [missing-argument]
21+
6 | func(*args) # error: [missing-argument]
22+
7 | func(**kwargs) # error: [missing-argument]
23+
8 | return wrapper
24+
```
25+
26+
# Diagnostics
27+
28+
```
29+
error[missing-argument]: No arguments provided for required parameters `*args`, `**kwargs`
30+
--> src/mdtest_snippet.py:5:9
31+
|
32+
3 | def decorator[**P](func: Callable[P, int]) -> Callable[P, None]:
33+
4 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
34+
5 | func() # error: [missing-argument]
35+
| ^^^^^^
36+
6 | func(*args) # error: [missing-argument]
37+
7 | func(**kwargs) # error: [missing-argument]
38+
|
39+
info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime
40+
info: rule `missing-argument` is enabled by default
41+
42+
```
43+
44+
```
45+
error[missing-argument]: No argument provided for required parameter `**kwargs`
46+
--> src/mdtest_snippet.py:6:9
47+
|
48+
4 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> None:
49+
5 | func() # error: [missing-argument]
50+
6 | func(*args) # error: [missing-argument]
51+
| ^^^^^^^^^^^
52+
7 | func(**kwargs) # error: [missing-argument]
53+
8 | return wrapper
54+
|
55+
info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime
56+
info: rule `missing-argument` is enabled by default
57+
58+
```
59+
60+
```
61+
error[missing-argument]: No argument provided for required parameter `*args`
62+
--> src/mdtest_snippet.py:7:9
63+
|
64+
5 | func() # error: [missing-argument]
65+
6 | func(*args) # error: [missing-argument]
66+
7 | func(**kwargs) # error: [missing-argument]
67+
| ^^^^^^^^^^^^^^
68+
8 | return wrapper
69+
|
70+
info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime
71+
info: rule `missing-argument` is enabled by default
72+
73+
```

crates/ty_python_semantic/src/types/call/bind.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3045,6 +3045,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
30453045
if !missing.is_empty() {
30463046
self.errors.push(BindingError::MissingArguments {
30473047
parameters: ParameterContexts(missing),
3048+
paramspec: self.parameters.as_paramspec(),
30483049
});
30493050
}
30503051

@@ -4215,6 +4216,10 @@ pub(crate) enum BindingError<'db> {
42154216
/// One or more required parameters (that is, with no default) is not supplied by any argument.
42164217
MissingArguments {
42174218
parameters: ParameterContexts,
4219+
/// If the missing arguments are for a `ParamSpec`, this contains the `ParamSpec` typevar.
4220+
/// This is used to provide more informative error messages explaining why `*args` and
4221+
/// `**kwargs` are required.
4222+
paramspec: Option<BoundTypeVarInstance<'db>>,
42184223
},
42194224
/// A call argument can't be matched to any parameter.
42204225
UnknownArgument {
@@ -4554,7 +4559,10 @@ impl<'db> BindingError<'db> {
45544559
}
45554560
}
45564561

4557-
Self::MissingArguments { parameters } => {
4562+
Self::MissingArguments {
4563+
parameters,
4564+
paramspec,
4565+
} => {
45584566
if let Some(builder) = context.report_lint(&MISSING_ARGUMENT, node) {
45594567
let s = if parameters.0.len() == 1 { "" } else { "s" };
45604568
let mut diag = builder.into_diagnostic(format_args!(
@@ -4581,6 +4589,16 @@ impl<'db> BindingError<'db> {
45814589
diag.sub(sub);
45824590
}
45834591
}
4592+
if let Some(paramspec) = paramspec {
4593+
let paramspec_name = paramspec.name(context.db());
4594+
diag.sub(SubDiagnostic::new(
4595+
SubDiagnosticSeverity::Info,
4596+
format_args!(
4597+
"These arguments are required because `ParamSpec` `{paramspec_name}` \
4598+
could represent any set of parameters at runtime"
4599+
),
4600+
));
4601+
}
45844602
}
45854603
}
45864604

0 commit comments

Comments
 (0)