diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument_paramspec.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument_paramspec.md new file mode 100644 index 00000000000000..f16dc52e9d38e2 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument_paramspec.md @@ -0,0 +1,22 @@ +# Missing argument for ParamSpec + + + +For `ParamSpec` callables, both `*args` and `**kwargs` are required since the underlying callable's +signature is unknown. We add a sub-diagnostic explaining why these parameters are required. + +```toml +[environment] +python-version = "3.12" +``` + +```py +from typing import Callable + +def decorator[**P](func: Callable[P, int]) -> Callable[P, None]: + def wrapper(*args: P.args, **kwargs: P.kwargs) -> None: + func() # error: [missing-argument] + func(*args) # error: [missing-argument] + func(**kwargs) # error: [missing-argument] + return wrapper +``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md index 6e31e3d7587d8d..76bb75c43c8e6a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/paramspec.md @@ -179,12 +179,15 @@ def f[**P](func: Callable[P, int]) -> Callable[P, None]: reveal_type(func(*kwargs, **args)) # revealed: int # error: [invalid-argument-type] "Argument is incorrect: Expected `P@f.args`, found `P@f.kwargs`" + # error: [missing-argument] reveal_type(func(args, kwargs)) # revealed: int # Both parameters are required - # TODO: error + # error: [missing-argument] reveal_type(func()) # revealed: int + # error: [missing-argument] reveal_type(func(*args)) # revealed: int + # error: [missing-argument] reveal_type(func(**kwargs)) # revealed: int return wrapper ``` diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument_par\342\200\246_-_Missing_argument_for\342\200\246_(b632d61c1d75f9fb).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument_par\342\200\246_-_Missing_argument_for\342\200\246_(b632d61c1d75f9fb).snap" new file mode 100644 index 00000000000000..d026666285f851 --- /dev/null +++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument_par\342\200\246_-_Missing_argument_for\342\200\246_(b632d61c1d75f9fb).snap" @@ -0,0 +1,73 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- + +--- +mdtest name: missing_argument_paramspec.md - Missing argument for ParamSpec +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument_paramspec.md +--- + +# Python source files + +## mdtest_snippet.py + +``` +1 | from typing import Callable +2 | +3 | def decorator[**P](func: Callable[P, int]) -> Callable[P, None]: +4 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> None: +5 | func() # error: [missing-argument] +6 | func(*args) # error: [missing-argument] +7 | func(**kwargs) # error: [missing-argument] +8 | return wrapper +``` + +# Diagnostics + +``` +error[missing-argument]: No arguments provided for required parameters `*args`, `**kwargs` + --> src/mdtest_snippet.py:5:9 + | +3 | def decorator[**P](func: Callable[P, int]) -> Callable[P, None]: +4 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> None: +5 | func() # error: [missing-argument] + | ^^^^^^ +6 | func(*args) # error: [missing-argument] +7 | func(**kwargs) # error: [missing-argument] + | +info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime +info: rule `missing-argument` is enabled by default + +``` + +``` +error[missing-argument]: No argument provided for required parameter `**kwargs` + --> src/mdtest_snippet.py:6:9 + | +4 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> None: +5 | func() # error: [missing-argument] +6 | func(*args) # error: [missing-argument] + | ^^^^^^^^^^^ +7 | func(**kwargs) # error: [missing-argument] +8 | return wrapper + | +info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime +info: rule `missing-argument` is enabled by default + +``` + +``` +error[missing-argument]: No argument provided for required parameter `*args` + --> src/mdtest_snippet.py:7:9 + | +5 | func() # error: [missing-argument] +6 | func(*args) # error: [missing-argument] +7 | func(**kwargs) # error: [missing-argument] + | ^^^^^^^^^^^^^^ +8 | return wrapper + | +info: These arguments are required because `ParamSpec` `P` could represent any set of parameters at runtime +info: rule `missing-argument` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 37ff6f297a015d..14239634b64933 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -3013,6 +3013,11 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { }); } + // For ParamSpec parameters, both *args and **kwargs are required since we don't know + // what arguments the underlying callable expects. For all other callables, variadic + // and keyword_variadic parameters are optional. + let paramspec_parameters = self.parameters.as_paramspec().is_some(); + let mut missing = vec![]; for ( index, @@ -3027,11 +3032,11 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { continue; } let param = &self.parameters[index]; - if param.is_variadic() - || param.is_keyword_variadic() + if !paramspec_parameters && (param.is_variadic() || param.is_keyword_variadic()) || param.default_type().is_some() { // variadic/keywords and defaulted arguments are not required + // (unless the parameters represent a ParamSpec) continue; } missing.push(ParameterContext::new(param, index, false)); @@ -3040,6 +3045,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> { if !missing.is_empty() { self.errors.push(BindingError::MissingArguments { parameters: ParameterContexts(missing), + paramspec: self.parameters.as_paramspec(), }); } @@ -4210,6 +4216,10 @@ pub(crate) enum BindingError<'db> { /// One or more required parameters (that is, with no default) is not supplied by any argument. MissingArguments { parameters: ParameterContexts, + /// If the missing arguments are for a `ParamSpec`, this contains the `ParamSpec` typevar. + /// This is used to provide more informative error messages explaining why `*args` and + /// `**kwargs` are required. + paramspec: Option>, }, /// A call argument can't be matched to any parameter. UnknownArgument { @@ -4549,7 +4559,10 @@ impl<'db> BindingError<'db> { } } - Self::MissingArguments { parameters } => { + Self::MissingArguments { + parameters, + paramspec, + } => { if let Some(builder) = context.report_lint(&MISSING_ARGUMENT, node) { let s = if parameters.0.len() == 1 { "" } else { "s" }; let mut diag = builder.into_diagnostic(format_args!( @@ -4576,6 +4589,16 @@ impl<'db> BindingError<'db> { diag.sub(sub); } } + if let Some(paramspec) = paramspec { + let paramspec_name = paramspec.name(context.db()); + diag.sub(SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!( + "These arguments are required because `ParamSpec` `{paramspec_name}` \ + could represent any set of parameters at runtime" + ), + )); + } } }