diff --git a/.github/workflows/ty-ecosystem-analyzer.yaml b/.github/workflows/ty-ecosystem-analyzer.yaml
index dccc9d1bb9..a1640e5974 100644
--- a/.github/workflows/ty-ecosystem-analyzer.yaml
+++ b/.github/workflows/ty-ecosystem-analyzer.yaml
@@ -67,7 +67,7 @@ jobs:
cd ..
- uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf"
+ uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@0aff03414da5d242e97a9f43fb502e085637a4a1"
ecosystem-analyzer \
--repository ruff \
diff --git a/.github/workflows/ty-ecosystem-report.yaml b/.github/workflows/ty-ecosystem-report.yaml
index 5a2521b02c..c9df4f1b28 100644
--- a/.github/workflows/ty-ecosystem-report.yaml
+++ b/.github/workflows/ty-ecosystem-report.yaml
@@ -52,7 +52,7 @@ jobs:
cd ..
- uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@11aa5472cf9d6b9e019c401505a093112942d7bf"
+ uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@0aff03414da5d242e97a9f43fb502e085637a4a1"
ecosystem-analyzer \
--verbose \
diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 58b1db584c..ab6973d5aa 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -39,7 +39,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -95,7 +95,7 @@ f(int) # error
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -126,7 +126,7 @@ a = 1
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -158,7 +158,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -190,7 +190,7 @@ class B(A): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -217,7 +217,7 @@ class B(A, A): ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -329,7 +329,7 @@ def test(): -> "Literal[5]":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -359,7 +359,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -385,7 +385,7 @@ t[3] # IndexError: tuple index out of range
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -474,7 +474,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -501,7 +501,7 @@ func("foo") # error: [invalid-argument-type]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -529,7 +529,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -563,7 +563,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -599,7 +599,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -623,7 +623,7 @@ class A(42): ... # error: [invalid-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -650,7 +650,7 @@ with 1:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -679,7 +679,7 @@ a: str
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -723,7 +723,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -756,7 +756,7 @@ class C[U](Generic[T]): ...
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -795,7 +795,7 @@ carol = Person(name="Carol", age=25) # typo!
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -830,7 +830,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -864,7 +864,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -896,7 +896,7 @@ TypeError: can only inherit from a NamedTuple type and Generic
Default level: error ·
Preview (since 1.0.0) ·
Related issues ·
-View source
+View source
@@ -926,7 +926,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -976,7 +976,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1002,7 +1002,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1033,7 +1033,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1067,7 +1067,7 @@ TypeError: Protocols can only inherit from other protocols, got
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1116,7 +1116,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1141,7 +1141,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1199,7 +1199,7 @@ TODO #14889
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -1226,7 +1226,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1256,7 +1256,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1286,7 +1286,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1320,7 +1320,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -1354,7 +1354,7 @@ class C:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1389,7 +1389,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1414,7 +1414,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1447,7 +1447,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1476,7 +1476,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1500,7 +1500,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1526,7 +1526,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1553,7 +1553,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -1611,7 +1611,7 @@ def test(): -> "int":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1641,7 +1641,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1670,7 +1670,7 @@ class B(A): ... # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1697,7 +1697,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1725,7 +1725,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1771,7 +1771,7 @@ class A:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1798,7 +1798,7 @@ f(x=1, y=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1826,7 +1826,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1851,7 +1851,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1876,7 +1876,7 @@ print(x) # NameError: name 'x' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1913,7 +1913,7 @@ b1 < b2 < b1 # exception raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1941,7 +1941,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1966,7 +1966,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
Default level: warn ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -2007,7 +2007,7 @@ class SubProto(BaseProto, Protocol):
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -2095,7 +2095,7 @@ a = 20 / 0 # type: ignore
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2123,7 +2123,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2155,7 +2155,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2187,7 +2187,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2214,7 +2214,7 @@ cast(int, f()) # Redundant
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2238,7 +2238,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -2296,7 +2296,7 @@ def g():
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -2335,7 +2335,7 @@ class D(C): ... # error: [unsupported-base]
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -2398,7 +2398,7 @@ def foo(x: int | str) -> int | str:
Default level: ignore ·
Preview (since 0.0.1-alpha.1) ·
Related issues ·
-View source
+View source
@@ -2422,7 +2422,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs
index 0edbc1c414..6640c4f598 100644
--- a/crates/ty/tests/cli/main.rs
+++ b/crates/ty/tests/cli/main.rs
@@ -41,22 +41,24 @@ fn test_quiet_output() -> anyhow::Result<()> {
let case = CliTest::with_file("test.py", "x: int = 'foo'")?;
// By default, we emit a diagnostic
- assert_cmd_snapshot!(case.command(), @r###"
+ assert_cmd_snapshot!(case.command(), @r#"
success: false
exit_code: 1
----- stdout -----
error[invalid-assignment]: Object of type `Literal["foo"]` is not assignable to `int`
- --> test.py:1:1
+ --> test.py:1:4
|
1 | x: int = 'foo'
- | ^
+ | --- ^^^^^ Incompatible value of type `Literal["foo"]`
+ | |
+ | Declared type
|
info: rule `invalid-assignment` is enabled by default
Found 1 diagnostic
----- stderr -----
- "###);
+ "#);
// With `quiet`, the diagnostic is not displayed, just the summary message
assert_cmd_snapshot!(case.command().arg("--quiet"), @r"
diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
new file mode 100644
index 0000000000..066a110f7d
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment.md
@@ -0,0 +1,52 @@
+# Invalid assignment 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).
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Annotated_assignment_(4b799ca1eeb857b9).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Annotated_assignment_(4b799ca1eeb857b9).snap
new file mode 100644
index 0000000000..b4d14282da
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Annotated_assignment_(4b799ca1eeb857b9).snap
@@ -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
+
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Multiline_expression…_(f316976ffe72c6c7).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Multiline_expression…_(f316976ffe72c6c7).snap
new file mode 100644
index 0000000000..9c908b4a3e
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Multiline_expression…_(f316976ffe72c6c7).snap
@@ -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
+
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Multiple_targets_(e20ddfd7a91affb0).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Multiple_targets_(e20ddfd7a91affb0).snap
new file mode 100644
index 0000000000..8ac6d47663
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Multiple_targets_(e20ddfd7a91affb0).snap
@@ -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
+
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Named_expression_(35c120b3bd9929f8).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Named_expression_(35c120b3bd9929f8).snap
new file mode 100644
index 0000000000..df49bebbc9
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Named_expression_(35c120b3bd9929f8).snap
@@ -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
+
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Unannotated_assignme…_(67e4b9239d5681a).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Unannotated_assignme…_(67e4b9239d5681a).snap
new file mode 100644
index 0000000000..03cbb5f6e2
--- /dev/null
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/invalid_assignment.m…_-_Invalid_assignment_d…_-_Unannotated_assignme…_(67e4b9239d5681a).snap
@@ -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
+
+```
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shado…_(c8ff9e3a079e8bd5).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shado…_(c8ff9e3a079e8bd5).snap
index 5109c05d9a..9c2fa5e925 100644
--- a/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shado…_(c8ff9e3a079e8bd5).snap
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_class_shado…_(c8ff9e3a079e8bd5).snap
@@ -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 ``
|
info: Annotate to make it explicit if this is intentional
info: rule `invalid-assignment` is enabled by default
diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_sh…_(a1515328b775ebc1).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_sh…_(a1515328b775ebc1).snap
index c9e3b2b326..e642105997 100644
--- a/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_sh…_(a1515328b775ebc1).snap
+++ b/crates/ty_python_semantic/resources/mdtest/snapshots/shadowing.md_-_Shadowing_-_Implicit_function_sh…_(a1515328b775ebc1).snap
@@ -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
diff --git a/crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md b/crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md
index a29277516d..a8077eba1f 100644
--- a/crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md
+++ b/crates/ty_python_semantic/resources/mdtest/stubs/ellipsis.md
@@ -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: ...
```
diff --git a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md
index 765d4f9e65..f9c2f9b5d2 100644
--- a/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md
+++ b/crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.md
@@ -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
+)
+```
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index 07ad837655..42b621dcab 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -31,8 +31,11 @@ use crate::{
};
use itertools::Itertools;
use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity};
+use ruff_db::source::source_text;
use ruff_python_ast::name::Name;
+use ruff_python_ast::parenthesize::parentheses_iterator;
use ruff_python_ast::{self as ast, AnyNodeRef, Identifier};
+use ruff_python_trivia::CommentRanges;
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet;
use std::fmt::Formatter;
@@ -2068,15 +2071,13 @@ pub(crate) fn is_invalid_typed_dict_literal(
&& matches!(source, AnyNodeRef::ExprDict(_))
}
-fn report_invalid_assignment_with_message(
- context: &InferContext,
- node: AnyNodeRef,
- target_ty: Type,
+fn report_invalid_assignment_with_message<'db, 'ctx: 'db, T: Ranged>(
+ context: &'ctx InferContext,
+ node: T,
+ target_ty: Type<'db>,
message: std::fmt::Arguments,
-) {
- let Some(builder) = context.report_lint(&INVALID_ASSIGNMENT, node) else {
- return;
- };
+) -> Option> {
+ let builder = context.report_lint(&INVALID_ASSIGNMENT, node)?;
match target_ty {
Type::ClassLiteral(class) => {
let mut diag = builder.into_diagnostic(format_args!(
@@ -2084,6 +2085,7 @@ fn report_invalid_assignment_with_message(
class.name(context.db()),
));
diag.info("Annotate to make it explicit if this is intentional");
+ Some(diag)
}
Type::FunctionLiteral(function) => {
let mut diag = builder.into_diagnostic(format_args!(
@@ -2091,53 +2093,106 @@ fn report_invalid_assignment_with_message(
function.name(context.db()),
));
diag.info("Annotate to make it explicit if this is intentional");
+ Some(diag)
}
+
_ => {
- builder.into_diagnostic(message);
+ let diag = builder.into_diagnostic(message);
+ Some(diag)
}
}
}
pub(super) fn report_invalid_assignment<'db>(
context: &InferContext<'db, '_>,
- node: AnyNodeRef,
+ target_node: AnyNodeRef,
definition: Definition<'db>,
target_ty: Type,
- mut source_ty: Type<'db>,
+ mut value_ty: Type<'db>,
) {
- let value_expr = match definition.kind(context.db()) {
+ let definition_kind = definition.kind(context.db());
+ let value_node = match definition_kind {
DefinitionKind::Assignment(def) => Some(def.value(context.module())),
DefinitionKind::AnnotatedAssignment(def) => def.value(context.module()),
DefinitionKind::NamedExpression(def) => Some(&*def.node(context.module()).value),
_ => None,
};
- if let Some(value_expr) = value_expr
- && is_invalid_typed_dict_literal(context.db(), target_ty, value_expr.into())
+ if let Some(value_node) = value_node
+ && is_invalid_typed_dict_literal(context.db(), target_ty, value_node.into())
{
return;
}
let settings =
- DisplaySettings::from_possibly_ambiguous_type_pair(context.db(), target_ty, source_ty);
+ DisplaySettings::from_possibly_ambiguous_type_pair(context.db(), target_ty, value_ty);
- if let Some(value_expr) = value_expr {
+ if let Some(value_node) = value_node {
// Re-infer the RHS of the annotated assignment, ignoring the type context for more precise
// error messages.
- source_ty =
- infer_isolated_expression(context.db(), definition.scope(context.db()), value_expr);
+ value_ty =
+ infer_isolated_expression(context.db(), definition.scope(context.db()), value_node);
}
- report_invalid_assignment_with_message(
+ let diagnostic_range = if let Some(value_node) = value_node {
+ // Expand the range to include parentheses around the value, if any. This allows
+ // invalid-assignment diagnostics to be suppressed on the opening or closing parenthesis:
+ // ```py
+ // x: str = ( # ty: ignore <- here
+ // 1 + 2 + 3
+ // ) # ty: ignore <- or here
+ // ```
+
+ let comment_ranges = CommentRanges::from(context.module().tokens());
+ let source = source_text(context.db(), context.file());
+ parentheses_iterator(value_node.into(), None, &comment_ranges, &source)
+ .last()
+ .unwrap_or(value_node.range())
+ } else {
+ target_node.range()
+ };
+
+ let Some(mut diag) = report_invalid_assignment_with_message(
context,
- node,
+ diagnostic_range,
target_ty,
format_args!(
"Object of type `{}` is not assignable to `{}`",
- source_ty.display_with(context.db(), settings.clone()),
+ value_ty.display_with(context.db(), settings.clone()),
target_ty.display_with(context.db(), settings)
),
- );
+ ) else {
+ return;
+ };
+
+ if value_node.is_some() {
+ match definition_kind {
+ DefinitionKind::AnnotatedAssignment(assignment) => {
+ // For annotated assignments, just point to the annotation in the source code.
+ diag.annotate(
+ context
+ .secondary(assignment.annotation(context.module()))
+ .message("Declared type"),
+ );
+ }
+ _ => {
+ // Otherwise, annotate the target with its declared type.
+ diag.annotate(context.secondary(target_node).message(format_args!(
+ "Declared type `{}`",
+ target_ty.display(context.db()),
+ )));
+ }
+ }
+
+ diag.set_primary_message(format_args!(
+ "Incompatible value of type `{}`",
+ value_ty.display(context.db()),
+ ));
+
+ // Overwrite the concise message to avoid showing the value type twice
+ let message = diag.primary_message().to_string();
+ diag.set_concise_message(message);
+ }
}
pub(super) fn report_invalid_attribute_assignment(
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index a99d9dfe21..43f569b9f2 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -7761,7 +7761,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
self.infer_expression(target, TypeContext::default());
- self.add_binding(named.into(), definition, |builder, tcx| {
+ self.add_binding(named.target.as_ref().into(), definition, |builder, tcx| {
builder.infer_expression(value, tcx)
})
}