red_knot_python_semantic: improve diagnostic message for "invalid argument type"

This uses the refactoring and support for secondary diagnostic messages
to improve the diagnostic for "invalid argument type." The main
improvement here is that we show where the function being called is
defined, and annotate the span corresponding to the invalid parameter.
This commit is contained in:
Andrew Gallant 2025-02-12 14:32:11 -05:00 committed by Andrew Gallant
parent 87668e24b1
commit 3ea32e2cdd
16 changed files with 817 additions and 1 deletions

View File

@ -0,0 +1,184 @@
# Invalid argument type diagnostics
<!-- snapshot-diagnostics -->
## Basic
This is a basic test demonstrating that a diagnostic points to the function definition corresponding
to the invalid argument.
```py
def foo(x: int) -> int:
return x * x
foo("hello") # error: [invalid-argument-type]
```
## Different source order
This is like the basic test, except we put the call site above the function definition.
```py
def bar():
foo("hello") # error: [invalid-argument-type]
def foo(x: int) -> int:
return x * x
```
## Different files
This tests that a diagnostic can point to a function definition in a different file in which an
invalid call site was found.
`package.py`:
```py
def foo(x: int) -> int:
return x * x
```
```py
import package
package.foo("hello") # error: [invalid-argument-type]
```
## Many parameters
This checks that a diagnostic renders reasonably when there are multiple parameters.
```py
def foo(x: int, y: int, z: int) -> int:
return x * y * z
foo(1, "hello", 3) # error: [invalid-argument-type]
```
## Many parameters across multiple lines
This checks that a diagnostic renders reasonably when there are multiple parameters spread out
across multiple lines.
```py
def foo(
x: int,
y: int,
z: int,
) -> int:
return x * y * z
foo(1, "hello", 3) # error: [invalid-argument-type]
```
## Many parameters with multiple invalid arguments
This checks that a diagnostic renders reasonably when there are multiple parameters and multiple
invalid argument types.
```py
def foo(x: int, y: int, z: int) -> int:
return x * y * z
# error: [invalid-argument-type]
# error: [invalid-argument-type]
# error: [invalid-argument-type]
foo("a", "b", "c")
```
At present (2025-02-18), this renders three different diagnostic messages. But arguably, these could
all be folded into one diagnostic. Fixing this requires at least better support for multi-spans in
the diagnostic model and possibly also how diagnostics are emitted by the type checker itself.
## Test calling a function whose type is vendored from `typeshed`
This tests that diagnostic rendering is reasonable when the function being called is from the
standard library.
```py
import json
json.loads(5) # error: [invalid-argument-type]
```
## Tests for a variety of argument types
These tests check that diagnostic output is reasonable regardless of the kinds of arguments used in
a function definition.
### Only positional
Tests a function definition with only positional parameters.
```py
def foo(x: int, y: int, z: int, /) -> int:
return x * y * z
foo(1, "hello", 3) # error: [invalid-argument-type]
```
### Variadic arguments
Tests a function definition with variadic arguments.
```py
def foo(*numbers: int) -> int:
return len(numbers)
foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
```
### Keyword only arguments
Tests a function definition with keyword-only arguments.
```py
def foo(x: int, y: int, *, z: int = 0) -> int:
return x * y * z
foo(1, 2, z="hello") # error: [invalid-argument-type]
```
### One keyword argument
Tests a function definition with keyword-only arguments.
```py
def foo(x: int, y: int, z: int = 0) -> int:
return x * y * z
foo(1, 2, "hello") # error: [invalid-argument-type]
```
### Variadic keyword arguments
```py
def foo(**numbers: int) -> int:
return len(numbers)
foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
```
### Mix of arguments
Tests a function definition with multiple different kinds of arguments.
```py
def foo(x: int, /, y: int, *, z: int = 0) -> int:
return x * y * z
foo(1, 2, z="hello") # error: [invalid-argument-type]
```
### Synthetic arguments
Tests a function call with synthetic arguments.
```py
class C:
def __call__(self, x: int) -> int:
return 1
c = C()
c("wrong") # error: [invalid-argument-type]
```

View File

@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Basic
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int) -> int:
2 | return x * x
3 |
4 | foo("hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:5
|
2 | return x * x
3 |
4 | foo("hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:9
|
1 | def foo(x: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * x
|
```

View File

@ -0,0 +1,45 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different files
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## package.py
```
1 | def foo(x: int) -> int:
2 | return x * x
```
## mdtest_snippet.py
```
1 | import package
2 |
3 | package.foo("hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:3:13
|
1 | import package
2 |
3 | package.foo("hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
::: /src/package.py:1:9
|
1 | def foo(x: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * x
|
```

View File

@ -0,0 +1,43 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Different source order
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def bar():
2 | foo("hello") # error: [invalid-argument-type]
3 |
4 | def foo(x: int) -> int:
5 | return x * x
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:2:9
|
1 | def bar():
2 | foo("hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
3 |
4 | def foo(x: int) -> int:
|
::: /src/mdtest_snippet.py:4:9
|
2 | foo("hello") # error: [invalid-argument-type]
3 |
4 | def foo(x: int) -> int:
| ------ info: parameter declared in function definition here
5 | return x * x
|
```

View File

@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, z: int) -> int:
2 | return x * y * z
3 |
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:8
|
2 | return x * y * z
3 |
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:17
|
1 | def foo(x: int, y: int, z: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@ -0,0 +1,46 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters across multiple lines
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(
2 | x: int,
3 | y: int,
4 | z: int,
5 | ) -> int:
6 | return x * y * z
7 |
8 | foo(1, "hello", 3) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:8:8
|
6 | return x * y * z
7 |
8 | foo(1, "hello", 3) # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:3:5
|
1 | def foo(
2 | x: int,
3 | y: int,
| ------ info: parameter declared in function definition here
4 | z: int,
5 | ) -> int:
|
```

View File

@ -0,0 +1,78 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Many parameters with multiple invalid arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, z: int) -> int:
2 | return x * y * z
3 |
4 | # error: [invalid-argument-type]
5 | # error: [invalid-argument-type]
6 | # error: [invalid-argument-type]
7 | foo("a", "b", "c")
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:7:5
|
5 | # error: [invalid-argument-type]
6 | # error: [invalid-argument-type]
7 | foo("a", "b", "c")
| ^^^ Object of type `Literal["a"]` cannot be assigned to parameter 1 (`x`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:9
|
1 | def foo(x: int, y: int, z: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:7:10
|
5 | # error: [invalid-argument-type]
6 | # error: [invalid-argument-type]
7 | foo("a", "b", "c")
| ^^^ Object of type `Literal["b"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:17
|
1 | def foo(x: int, y: int, z: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:7:15
|
5 | # error: [invalid-argument-type]
6 | # error: [invalid-argument-type]
7 | foo("a", "b", "c")
| ^^^ Object of type `Literal["c"]` cannot be assigned to parameter 3 (`z`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:25
|
1 | def foo(x: int, y: int, z: int) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@ -0,0 +1,41 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Test calling a function whose type is vendored from `typeshed`
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | import json
2 |
3 | json.loads(5) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:3:12
|
1 | import json
2 |
3 | json.loads(5) # error: [invalid-argument-type]
| ^ Object of type `Literal[5]` cannot be assigned to parameter 1 (`s`) of function `loads`; expected type `str | bytes | bytearray`
|
::: vendored://stdlib/json/__init__.pyi:40:5
|
38 | ) -> None: ...
39 | def loads(
40 | s: str | bytes | bytearray,
| -------------------------- info: parameter declared in function definition here
41 | *,
42 | cls: type[JSONDecoder] | None = None,
|
```

View File

@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Keyword only arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, *, z: int = 0) -> int:
2 | return x * y * z
3 |
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:11
|
2 | return x * y * z
3 |
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `z` of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:28
|
1 | def foo(x: int, y: int, *, z: int = 0) -> int:
| ---------- info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Mix of arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, /, y: int, *, z: int = 0) -> int:
2 | return x * y * z
3 |
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:11
|
2 | return x * y * z
3 |
4 | foo(1, 2, z="hello") # error: [invalid-argument-type]
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `z` of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:31
|
1 | def foo(x: int, /, y: int, *, z: int = 0) -> int:
| ---------- info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - One keyword argument
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, z: int = 0) -> int:
2 | return x * y * z
3 |
4 | foo(1, 2, "hello") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:11
|
2 | return x * y * z
3 |
4 | foo(1, 2, "hello") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 3 (`z`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:25
|
1 | def foo(x: int, y: int, z: int = 0) -> int:
| ---------- info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Only positional
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(x: int, y: int, z: int, /) -> int:
2 | return x * y * z
3 |
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:8
|
2 | return x * y * z
3 |
4 | foo(1, "hello", 3) # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter 2 (`y`) of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:17
|
1 | def foo(x: int, y: int, z: int, /) -> int:
| ------ info: parameter declared in function definition here
2 | return x * y * z
|
```

View File

@ -0,0 +1,41 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Synthetic arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | class C:
2 | def __call__(self, x: int) -> int:
3 | return 1
4 |
5 | c = C()
6 | c("wrong") # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:6:3
|
5 | c = C()
6 | c("wrong") # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["wrong"]` cannot be assigned to parameter 2 (`x`) of function `__call__`; expected type `int`
|
::: /src/mdtest_snippet.py:2:24
|
1 | class C:
2 | def __call__(self, x: int) -> int:
| ------ info: parameter declared in function definition here
3 | return 1
|
```

View File

@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(*numbers: int) -> int:
2 | return len(numbers)
3 |
4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:14
|
2 | return len(numbers)
3 |
4 | foo(1, 2, 3, "hello", 5) # error: [invalid-argument-type]
| ^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `*numbers` of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:9
|
1 | def foo(*numbers: int) -> int:
| ------------- info: parameter declared in function definition here
2 | return len(numbers)
|
```

View File

@ -0,0 +1,39 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_argument_type.md - Invalid argument type diagnostics - Tests for a variety of argument types - Variadic keyword arguments
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/invalid_argument_type.md
---
# Python source files
## mdtest_snippet.py
```
1 | def foo(**numbers: int) -> int:
2 | return len(numbers)
3 |
4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
```
# Diagnostics
```
error: lint:invalid-argument-type
--> /src/mdtest_snippet.py:4:20
|
2 | return len(numbers)
3 |
4 | foo(a=1, b=2, c=3, d="hello", e=5) # error: [invalid-argument-type]
| ^^^^^^^^^ Object of type `Literal["hello"]` cannot be assigned to parameter `**numbers` of function `foo`; expected type `int`
|
::: /src/mdtest_snippet.py:1:9
|
1 | def foo(**numbers: int) -> int:
| -------------- info: parameter declared in function definition here
2 | return len(numbers)
|
```

View File

@ -6,7 +6,9 @@ use crate::types::diagnostic::{
};
use crate::types::signatures::Parameter;
use crate::types::{todo_type, UnionType};
use ruff_db::diagnostic::{SecondaryDiagnosticMessage, Span};
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
/// Bind a [`CallArguments`] against a callable [`Signature`].
///
@ -76,6 +78,7 @@ pub(crate) fn bind_call<'db>(
if let Some(expected_ty) = parameter.annotated_type() {
if !argument_ty.is_assignable_to(db, expected_ty) {
errors.push(CallBindingError::InvalidArgumentType {
callable_ty,
parameter: ParameterContext::new(parameter, index, positional),
argument_index: get_argument_index(argument_index, num_synthetic_args),
expected_ty,
@ -269,6 +272,7 @@ pub(crate) enum CallBindingError<'db> {
/// The type of an argument is not assignable to the annotated type of its corresponding
/// parameter.
InvalidArgumentType {
callable_ty: Type<'db>,
parameter: ParameterContext,
argument_index: Option<usize>,
expected_ty: Type<'db>,
@ -303,14 +307,35 @@ impl<'db> CallBindingError<'db> {
) {
match self {
Self::InvalidArgumentType {
callable_ty,
parameter,
argument_index,
expected_ty,
provided_ty,
} => {
let mut messages = vec![];
if let Some(func_lit) = callable_ty.into_function_literal() {
let func_lit_scope = func_lit.body_scope(context.db());
let mut span = Span::from(func_lit_scope.file(context.db()));
let node = func_lit_scope.node(context.db());
if let Some(func_def) = node.as_function() {
let range = func_def
.parameters
.iter()
.nth(parameter.index)
.map(|param| param.range())
.unwrap_or(func_def.parameters.range);
span = span.with_range(range);
messages.push(SecondaryDiagnosticMessage::new(
span,
"parameter declared in function definition here",
));
}
}
let provided_ty_display = provided_ty.display(context.db());
let expected_ty_display = expected_ty.display(context.db());
context.report_lint(
context.report_lint_with_secondary_messages(
&INVALID_ARGUMENT_TYPE,
Self::get_node(node, *argument_index),
format_args!(
@ -322,6 +347,7 @@ impl<'db> CallBindingError<'db> {
String::new()
}
),
messages,
);
}