[ty] Better invalid-assignment diagnostics (#21476)
## Summary Improve the diagnostic range for `invalid-assignment` diagnostics, and add source annotations for the value and target type. closes https://github.com/astral-sh/ty/issues/1556 ### Before <img width="836" height="601" alt="image" src="https://github.com/user-attachments/assets/a48219bb-58a8-4a83-b290-d09ef50ce5f0" /> ### After <img width="857" height="742" alt="image" src="https://github.com/user-attachments/assets/cfcaa4f4-94fb-459e-8d64-97050dfecb50" /> ## Ecosystem impact Very good! Due to the wider diagnostic range, we now pick up more `# type: ignore` directives that were supposed to suppress an invalid assignment diagnostic. ## Test Plan New snapshot tests
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# Invalid assignment diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Annotated assignment
|
||||
|
||||
```py
|
||||
x: int = "three" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Unannotated assignment
|
||||
|
||||
```py
|
||||
x: int
|
||||
x = "three" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Named expression
|
||||
|
||||
```py
|
||||
x: int
|
||||
|
||||
(x := "three") # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Multiline expressions
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
|
||||
# error: [invalid-assignment]
|
||||
x: str = (
|
||||
1 + 2 + (
|
||||
3 + 4 + 5
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Multiple targets
|
||||
|
||||
```py
|
||||
x: int
|
||||
y: str
|
||||
|
||||
x, y = ("a", "b") # error: [invalid-assignment]
|
||||
|
||||
x, y = (0, 0) # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Shadowing of classes and functions
|
||||
|
||||
See [shadowing.md](./shadowing.md).
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Annotated assignment
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | x: int = "three" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int`
|
||||
--> src/mdtest_snippet.py:1:4
|
||||
|
|
||||
1 | x: int = "three" # error: [invalid-assignment]
|
||||
| --- ^^^^^^^ Incompatible value of type `Literal["three"]`
|
||||
| |
|
||||
| Declared type
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Multiline expressions
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | # fmt: off
|
||||
2 |
|
||||
3 | # error: [invalid-assignment]
|
||||
4 | x: str = (
|
||||
5 | 1 + 2 + (
|
||||
6 | 3 + 4 + 5
|
||||
7 | )
|
||||
8 | )
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `Literal[15]` is not assignable to `str`
|
||||
--> src/mdtest_snippet.py:4:4
|
||||
|
|
||||
3 | # error: [invalid-assignment]
|
||||
4 | x: str = (
|
||||
| ____---___^
|
||||
| | |
|
||||
| | Declared type
|
||||
5 | | 1 + 2 + (
|
||||
6 | | 3 + 4 + 5
|
||||
7 | | )
|
||||
8 | | )
|
||||
| |_^ Incompatible value of type `Literal[15]`
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Multiple targets
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | x: int
|
||||
2 | y: str
|
||||
3 |
|
||||
4 | x, y = ("a", "b") # error: [invalid-assignment]
|
||||
5 |
|
||||
6 | x, y = (0, 0) # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `tuple[Literal["a"], Literal["b"]]` is not assignable to `int`
|
||||
--> src/mdtest_snippet.py:4:1
|
||||
|
|
||||
2 | y: str
|
||||
3 |
|
||||
4 | x, y = ("a", "b") # error: [invalid-assignment]
|
||||
| - ^^^^^^^^^^ Incompatible value of type `tuple[Literal["a"], Literal["b"]]`
|
||||
| |
|
||||
| Declared type `int`
|
||||
5 |
|
||||
6 | x, y = (0, 0) # error: [invalid-assignment]
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `tuple[Literal[0], Literal[0]]` is not assignable to `str`
|
||||
--> src/mdtest_snippet.py:6:4
|
||||
|
|
||||
4 | x, y = ("a", "b") # error: [invalid-assignment]
|
||||
5 |
|
||||
6 | x, y = (0, 0) # error: [invalid-assignment]
|
||||
| - ^^^^^^ Incompatible value of type `tuple[Literal[0], Literal[0]]`
|
||||
| |
|
||||
| Declared type `str`
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Named expression
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | x: int
|
||||
2 |
|
||||
3 | (x := "three") # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int`
|
||||
--> src/mdtest_snippet.py:3:2
|
||||
|
|
||||
1 | x: int
|
||||
2 |
|
||||
3 | (x := "three") # error: [invalid-assignment]
|
||||
| - ^^^^^^^ Incompatible value of type `Literal["three"]`
|
||||
| |
|
||||
| Declared type `int`
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_assignment.md - Invalid assignment diagnostics - Unannotated assignment
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | x: int
|
||||
2 | x = "three" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-assignment]: Object of type `Literal["three"]` is not assignable to `int`
|
||||
--> src/mdtest_snippet.py:2:1
|
||||
|
|
||||
1 | x: int
|
||||
2 | x = "three" # error: [invalid-assignment]
|
||||
| - ^^^^^^^ Incompatible value of type `Literal["three"]`
|
||||
| |
|
||||
| Declared type `int`
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
```
|
||||
@@ -26,7 +26,9 @@ error[invalid-assignment]: Implicit shadowing of class `C`
|
||||
1 | class C: ...
|
||||
2 |
|
||||
3 | C = 1 # error: [invalid-assignment]
|
||||
| ^
|
||||
| - ^ Incompatible value of type `Literal[1]`
|
||||
| |
|
||||
| Declared type `<class 'C'>`
|
||||
|
|
||||
info: Annotate to make it explicit if this is intentional
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
@@ -26,7 +26,9 @@ error[invalid-assignment]: Implicit shadowing of function `f`
|
||||
1 | def f(): ...
|
||||
2 |
|
||||
3 | f = 1 # error: [invalid-assignment]
|
||||
| ^
|
||||
| - ^ Incompatible value of type `Literal[1]`
|
||||
| |
|
||||
| Declared type `def f() -> Unknown`
|
||||
|
|
||||
info: Annotate to make it explicit if this is intentional
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
@@ -59,10 +59,10 @@ In a non-stub file, there's no special treatment of ellipsis literals. An ellips
|
||||
be assigned if `EllipsisType` is actually assignable to the annotated type.
|
||||
|
||||
```py
|
||||
# error: 7 [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`"
|
||||
# error: [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`"
|
||||
def f(x: int = ...) -> None: ...
|
||||
|
||||
# error: 1 [invalid-assignment] "Object of type `EllipsisType` is not assignable to `int`"
|
||||
# error: [invalid-assignment] "Object of type `EllipsisType` is not assignable to `int`"
|
||||
a: int = ...
|
||||
b = ...
|
||||
reveal_type(b) # revealed: EllipsisType
|
||||
@@ -73,6 +73,6 @@ reveal_type(b) # revealed: EllipsisType
|
||||
There is no special treatment of the builtin name `Ellipsis` in stubs, only of `...` literals.
|
||||
|
||||
```pyi
|
||||
# error: 7 [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`"
|
||||
# error: [invalid-parameter-default] "Default value of type `EllipsisType` is not assignable to annotated parameter type `int`"
|
||||
def f(x: int = Ellipsis) -> None: ...
|
||||
```
|
||||
|
||||
@@ -189,3 +189,40 @@ a = 10 + 4 # ty: ignore[division-by-zer]
|
||||
# error: [division-by-zero]
|
||||
a = 10 / 0 # ty: ignore[lint:division-by-zero]
|
||||
```
|
||||
|
||||
## Suppression of specific diagnostics
|
||||
|
||||
In this section, we make sure that specific diagnostics can be suppressed in various forms that
|
||||
users might expect to work.
|
||||
|
||||
### Invalid assignment
|
||||
|
||||
An invalid assignment can be suppressed in the following ways:
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
|
||||
x1: str = 1 + 2 + 3 # ty: ignore
|
||||
|
||||
x2: str = ( # ty: ignore
|
||||
1 + 2 + 3
|
||||
)
|
||||
|
||||
x4: str = (
|
||||
1 + 2 + 3
|
||||
) # ty: ignore
|
||||
```
|
||||
|
||||
It can *not* be suppressed by putting the `# ty: ignore` on the inner expression. The range targeted
|
||||
by the suppression comment needs to overlap with one of the boundaries of the value range (the outer
|
||||
parentheses in this case):
|
||||
|
||||
```py
|
||||
# fmt: off
|
||||
|
||||
# error: [invalid-assignment]
|
||||
x4: str = (
|
||||
# error: [unused-ignore-comment]
|
||||
1 + 2 + 3 # ty: ignore
|
||||
)
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user