diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md new file mode 100644 index 0000000000..24a6e552b2 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md @@ -0,0 +1,37 @@ +# Missing argument diagnostics + + + +If a non-union callable is called with a required parameter missing, we add a subdiagnostic showing +where the parameter was defined. We don't do this for unions as we currently emit a separate +diagnostic for each element of the union; having a sub-diagnostic for each element would probably be +too verbose for it to be worth it. + +`module.py`: + +```py +def f(a, b=42): ... +def g(a, b): ... + +class Foo: + def method(self, a): ... +``` + +`main.py`: + +```py +from module import f, g, Foo + +f() # error: [missing-argument] + +def coinflip() -> bool: + return True + +h = f if coinflip() else g + +# error: [missing-argument] +# error: [missing-argument] +h(b=56) + +Foo().method() # error: [missing-argument] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md new file mode 100644 index 0000000000..07eebd1c8c --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md @@ -0,0 +1,37 @@ +# too-many-positional-arguments diagnostics + + + +If a non-union callable is called with too many positional arguments, we add a subdiagnostic showing +where the callable was defined. We don't do this for unions as we currently emit a separate +diagnostic for each element of the union; having a sub-diagnostic for each element would probably be +too verbose for it to be worth it. + +`module.py`: + +```py +def f(a, b=42): ... +def g(a, b): ... + +class Foo: + def method(self, a): ... +``` + +`main.py`: + +```py +from module import f, g, Foo + +f(1, 2, 3) # error: [too-many-positional-arguments] + +def coinflip() -> bool: + return True + +h = f if coinflip() else g + +# error: [too-many-positional-arguments] +# error: [too-many-positional-arguments] +h(1, 2, 3) + +Foo().method(1, 2) # error: [too-many-positional-arguments] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/unknown_argument.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unknown_argument.md new file mode 100644 index 0000000000..5e709233ec --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unknown_argument.md @@ -0,0 +1,37 @@ +# Unknown argument diagnostics + + + +If a non-union callable is called with a parameter that doesn't match any parameter from the +signature, we add a subdiagnostic showing where the callable was defined. We don't do this for +unions as we currently emit a separate diagnostic for each element of the union; having a +sub-diagnostic for each element would probably be too verbose for it to be worth it. + +`module.py`: + +```py +def f(a, b, c=42): ... +def g(a, b): ... + +class Foo: + def method(self, a, b): ... +``` + +`main.py`: + +```py +from module import f, g, Foo + +f(a=1, b=2, c=3, d=42) # error: [unknown-argument] + +def coinflip() -> bool: + return True + +h = f if coinflip() else g + +# error: [unknown-argument] +# error: [unknown-argument] +h(a=1, b=2, d=42) + +Foo().method(a=1, b=2, c=3) # error: [unknown-argument] +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr…_-_Syntax_(142fa2948c3c6cf1).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr…_-_Syntax_(142fa2948c3c6cf1).snap index ac29feddc9..1a32df61af 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr…_-_Syntax_(142fa2948c3c6cf1).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/deprecated.md_-_Tests_for_the_`@depr…_-_Syntax_(142fa2948c3c6cf1).snap @@ -90,6 +90,16 @@ error[missing-argument]: No argument provided for required parameter `arg` of bo | ^^^^^^^^^^^^^^ 7 | from typing_extensions import deprecated | +info: Parameter declared here + --> stdlib/typing_extensions.pyi:967:28 + | +965 | stacklevel: int +966 | def __init__(self, message: LiteralString, /, *, category: type[Warning] | None = ..., stacklevel: int = 1) -> None: ... +967 | def __call__(self, arg: _T, /) -> _T: ... + | ^^^^^^^ +968 | +969 | @final + | info: rule `missing-argument` is enabled by default ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ…_-_Invalid_argument_typ…_-_Calls_to_methods_(4b3b8695d519a02).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ…_-_Invalid_argument_typ…_-_Calls_to_methods_(4b3b8695d519a02).snap index e0483e581d..a02a98d391 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ…_-_Invalid_argument_typ…_-_Calls_to_methods_(4b3b8695d519a02).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ…_-_Invalid_argument_typ…_-_Calls_to_methods_(4b3b8695d519a02).snap @@ -30,7 +30,7 @@ error[invalid-argument-type]: Argument to bound method `square` is incorrect 6 | c.square("hello") # error: [invalid-argument-type] | ^^^^^^^ Expected `int`, found `Literal["hello"]` | -info: Function defined here +info: Method defined here --> src/mdtest_snippet.py:2:9 | 1 | class C: diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ…_-_Invalid_argument_typ…_-_Tests_for_a_variety_…_-_Synthetic_arguments_(4c09844bbbf47741).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ…_-_Invalid_argument_typ…_-_Tests_for_a_variety_…_-_Synthetic_arguments_(4c09844bbbf47741).snap index a21d9085a5..5864211fc9 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ…_-_Invalid_argument_typ…_-_Tests_for_a_variety_…_-_Synthetic_arguments_(4c09844bbbf47741).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_argument_typ…_-_Invalid_argument_typ…_-_Tests_for_a_variety_…_-_Synthetic_arguments_(4c09844bbbf47741).snap @@ -30,7 +30,7 @@ error[invalid-argument-type]: Argument to bound method `__call__` is incorrect 6 | c("wrong") # error: [invalid-argument-type] | ^^^^^^^ Expected `int`, found `Literal["wrong"]` | -info: Function defined here +info: Method defined here --> src/mdtest_snippet.py:2:9 | 1 | class C: diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument.md_-_Missing_argument_dia…_(f0811e84fcea1085).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument.md_-_Missing_argument_dia…_(f0811e84fcea1085).snap new file mode 100644 index 0000000000..5311a64551 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/missing_argument.md_-_Missing_argument_dia…_(f0811e84fcea1085).snap @@ -0,0 +1,117 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: missing_argument.md - Missing argument diagnostics +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/missing_argument.md +--- + +# Python source files + +## module.py + +``` +1 | def f(a, b=42): ... +2 | def g(a, b): ... +3 | +4 | class Foo: +5 | def method(self, a): ... +``` + +## main.py + +``` + 1 | from module import f, g, Foo + 2 | + 3 | f() # error: [missing-argument] + 4 | + 5 | def coinflip() -> bool: + 6 | return True + 7 | + 8 | h = f if coinflip() else g + 9 | +10 | # error: [missing-argument] +11 | # error: [missing-argument] +12 | h(b=56) +13 | +14 | Foo().method() # error: [missing-argument] +``` + +# Diagnostics + +``` +error[missing-argument]: No argument provided for required parameter `a` of function `f` + --> src/main.py:3:1 + | +1 | from module import f, g, Foo +2 | +3 | f() # error: [missing-argument] + | ^^^ +4 | +5 | def coinflip() -> bool: + | +info: Parameter declared here + --> src/module.py:1:7 + | +1 | def f(a, b=42): ... + | ^ +2 | def g(a, b): ... + | +info: rule `missing-argument` is enabled by default + +``` + +``` +error[missing-argument]: No argument provided for required parameter `a` of function `f` + --> src/main.py:12:1 + | +10 | # error: [missing-argument] +11 | # error: [missing-argument] +12 | h(b=56) + | ^^^^^^^ +13 | +14 | Foo().method() # error: [missing-argument] + | +info: Union variant `def f(a, b=Literal[42]) -> Unknown` is incompatible with this call site +info: Attempted to call union type `(def f(a, b=Literal[42]) -> Unknown) | (def g(a, b) -> Unknown)` +info: rule `missing-argument` is enabled by default + +``` + +``` +error[missing-argument]: No argument provided for required parameter `a` of function `g` + --> src/main.py:12:1 + | +10 | # error: [missing-argument] +11 | # error: [missing-argument] +12 | h(b=56) + | ^^^^^^^ +13 | +14 | Foo().method() # error: [missing-argument] + | +info: Union variant `def g(a, b) -> Unknown` is incompatible with this call site +info: Attempted to call union type `(def f(a, b=Literal[42]) -> Unknown) | (def g(a, b) -> Unknown)` +info: rule `missing-argument` is enabled by default + +``` + +``` +error[missing-argument]: No argument provided for required parameter `a` of bound method `method` + --> src/main.py:14:1 + | +12 | h(b=56) +13 | +14 | Foo().method() # error: [missing-argument] + | ^^^^^^^^^^^^^^ + | +info: Parameter declared here + --> src/module.py:5:22 + | +4 | class Foo: +5 | def method(self, a): ... + | ^ + | +info: rule `missing-argument` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/too_many_positionals…_-_too-many-positional-…_(eafa522239b42502).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/too_many_positionals…_-_too-many-positional-…_(eafa522239b42502).snap new file mode 100644 index 0000000000..85d54b772b --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/too_many_positionals…_-_too-many-positional-…_(eafa522239b42502).snap @@ -0,0 +1,117 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: too_many_positionals.md - too-many-positional-arguments diagnostics +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/too_many_positionals.md +--- + +# Python source files + +## module.py + +``` +1 | def f(a, b=42): ... +2 | def g(a, b): ... +3 | +4 | class Foo: +5 | def method(self, a): ... +``` + +## main.py + +``` + 1 | from module import f, g, Foo + 2 | + 3 | f(1, 2, 3) # error: [too-many-positional-arguments] + 4 | + 5 | def coinflip() -> bool: + 6 | return True + 7 | + 8 | h = f if coinflip() else g + 9 | +10 | # error: [too-many-positional-arguments] +11 | # error: [too-many-positional-arguments] +12 | h(1, 2, 3) +13 | +14 | Foo().method(1, 2) # error: [too-many-positional-arguments] +``` + +# Diagnostics + +``` +error[too-many-positional-arguments]: Too many positional arguments to function `f`: expected 2, got 3 + --> src/main.py:3:9 + | +1 | from module import f, g, Foo +2 | +3 | f(1, 2, 3) # error: [too-many-positional-arguments] + | ^ +4 | +5 | def coinflip() -> bool: + | +info: Function signature here + --> src/module.py:1:5 + | +1 | def f(a, b=42): ... + | ^^^^^^^^^^ +2 | def g(a, b): ... + | +info: rule `too-many-positional-arguments` is enabled by default + +``` + +``` +error[too-many-positional-arguments]: Too many positional arguments to function `f`: expected 2, got 3 + --> src/main.py:12:9 + | +10 | # error: [too-many-positional-arguments] +11 | # error: [too-many-positional-arguments] +12 | h(1, 2, 3) + | ^ +13 | +14 | Foo().method(1, 2) # error: [too-many-positional-arguments] + | +info: Union variant `def f(a, b=Literal[42]) -> Unknown` is incompatible with this call site +info: Attempted to call union type `(def f(a, b=Literal[42]) -> Unknown) | (def g(a, b) -> Unknown)` +info: rule `too-many-positional-arguments` is enabled by default + +``` + +``` +error[too-many-positional-arguments]: Too many positional arguments to function `g`: expected 2, got 3 + --> src/main.py:12:9 + | +10 | # error: [too-many-positional-arguments] +11 | # error: [too-many-positional-arguments] +12 | h(1, 2, 3) + | ^ +13 | +14 | Foo().method(1, 2) # error: [too-many-positional-arguments] + | +info: Union variant `def g(a, b) -> Unknown` is incompatible with this call site +info: Attempted to call union type `(def f(a, b=Literal[42]) -> Unknown) | (def g(a, b) -> Unknown)` +info: rule `too-many-positional-arguments` is enabled by default + +``` + +``` +error[too-many-positional-arguments]: Too many positional arguments to bound method `method`: expected 2, got 3 + --> src/main.py:14:17 + | +12 | h(1, 2, 3) +13 | +14 | Foo().method(1, 2) # error: [too-many-positional-arguments] + | ^ + | +info: Method signature here + --> src/module.py:5:9 + | +4 | class Foo: +5 | def method(self, a): ... + | ^^^^^^^^^^^^^^^ + | +info: rule `too-many-positional-arguments` is enabled by default + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/unknown_argument.md_-_Unknown_argument_dia…_(f419c2a8e2ce2412).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/unknown_argument.md_-_Unknown_argument_dia…_(f419c2a8e2ce2412).snap new file mode 100644 index 0000000000..e311271fe8 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/unknown_argument.md_-_Unknown_argument_dia…_(f419c2a8e2ce2412).snap @@ -0,0 +1,117 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: unknown_argument.md - Unknown argument diagnostics +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/unknown_argument.md +--- + +# Python source files + +## module.py + +``` +1 | def f(a, b, c=42): ... +2 | def g(a, b): ... +3 | +4 | class Foo: +5 | def method(self, a, b): ... +``` + +## main.py + +``` + 1 | from module import f, g, Foo + 2 | + 3 | f(a=1, b=2, c=3, d=42) # error: [unknown-argument] + 4 | + 5 | def coinflip() -> bool: + 6 | return True + 7 | + 8 | h = f if coinflip() else g + 9 | +10 | # error: [unknown-argument] +11 | # error: [unknown-argument] +12 | h(a=1, b=2, d=42) +13 | +14 | Foo().method(a=1, b=2, c=3) # error: [unknown-argument] +``` + +# Diagnostics + +``` +error[unknown-argument]: Argument `d` does not match any known parameter of function `f` + --> src/main.py:3:18 + | +1 | from module import f, g, Foo +2 | +3 | f(a=1, b=2, c=3, d=42) # error: [unknown-argument] + | ^^^^ +4 | +5 | def coinflip() -> bool: + | +info: Function signature here + --> src/module.py:1:5 + | +1 | def f(a, b, c=42): ... + | ^^^^^^^^^^^^^ +2 | def g(a, b): ... + | +info: rule `unknown-argument` is enabled by default + +``` + +``` +error[unknown-argument]: Argument `d` does not match any known parameter of function `f` + --> src/main.py:12:13 + | +10 | # error: [unknown-argument] +11 | # error: [unknown-argument] +12 | h(a=1, b=2, d=42) + | ^^^^ +13 | +14 | Foo().method(a=1, b=2, c=3) # error: [unknown-argument] + | +info: Union variant `def f(a, b, c=Literal[42]) -> Unknown` is incompatible with this call site +info: Attempted to call union type `(def f(a, b, c=Literal[42]) -> Unknown) | (def g(a, b) -> Unknown)` +info: rule `unknown-argument` is enabled by default + +``` + +``` +error[unknown-argument]: Argument `d` does not match any known parameter of function `g` + --> src/main.py:12:13 + | +10 | # error: [unknown-argument] +11 | # error: [unknown-argument] +12 | h(a=1, b=2, d=42) + | ^^^^ +13 | +14 | Foo().method(a=1, b=2, c=3) # error: [unknown-argument] + | +info: Union variant `def g(a, b) -> Unknown` is incompatible with this call site +info: Attempted to call union type `(def f(a, b, c=Literal[42]) -> Unknown) | (def g(a, b) -> Unknown)` +info: rule `unknown-argument` is enabled by default + +``` + +``` +error[unknown-argument]: Argument `c` does not match any known parameter of bound method `method` + --> src/main.py:14:24 + | +12 | h(a=1, b=2, d=42) +13 | +14 | Foo().method(a=1, b=2, c=3) # error: [unknown-argument] + | ^^^ + | +info: Method signature here + --> src/module.py:5:9 + | +4 | class Foo: +5 | def method(self, a, b): ... + | ^^^^^^^^^^^^^^^^^^ + | +info: rule `unknown-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 da685d68ed..8d728840d9 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -2759,6 +2759,12 @@ impl<'db> BindingError<'db> { union_diag: Option<&UnionDiagnostic<'_, '_>>, matching_overload: Option<&MatchingOverloadLiteral<'_>>, ) { + let callable_kind = match callable_ty { + Type::FunctionLiteral(_) => "Function", + Type::BoundMethod(_) => "Method", + _ => "Callable", + }; + match self { Self::InvalidArgumentType { parameter, @@ -2831,8 +2837,10 @@ impl<'db> BindingError<'db> { } else if let Some((name_span, parameter_span)) = callable_ty.parameter_span(context.db(), Some(parameter.index)) { - let mut sub = - SubDiagnostic::new(SubDiagnosticSeverity::Info, "Function defined here"); + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!("{callable_kind} defined here"), + ); sub.annotate(Annotation::primary(name_span)); sub.annotate( Annotation::secondary(parameter_span).message("Parameter declared here"), @@ -2863,6 +2871,13 @@ impl<'db> BindingError<'db> { )); if let Some(union_diag) = union_diag { union_diag.add_union_context(context.db(), &mut diag); + } else if let Some(spans) = callable_ty.function_spans(context.db()) { + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!("{callable_kind} signature here"), + ); + sub.annotate(Annotation::primary(spans.signature)); + diag.sub(sub); } } } @@ -2880,6 +2895,19 @@ impl<'db> BindingError<'db> { )); if let Some(union_diag) = union_diag { union_diag.add_union_context(context.db(), &mut diag); + } else { + let span = callable_ty.parameter_span( + context.db(), + (parameters.0.len() == 1).then(|| parameters.0[0].index), + ); + if let Some((_, parameter_span)) = span { + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!("Parameter{s} declared here"), + ); + sub.annotate(Annotation::primary(parameter_span)); + diag.sub(sub); + } } } } @@ -2900,6 +2928,13 @@ impl<'db> BindingError<'db> { )); if let Some(union_diag) = union_diag { union_diag.add_union_context(context.db(), &mut diag); + } else if let Some(spans) = callable_ty.function_spans(context.db()) { + let mut sub = SubDiagnostic::new( + SubDiagnosticSeverity::Info, + format_args!("{callable_kind} signature here"), + ); + sub.annotate(Annotation::primary(spans.signature)); + diag.sub(sub); } } }