Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could easily add more exhaustive tests to almost all subsections. But I'd rather do that while working on the actual implementation, instead of coming up with all corner cases right away. For now, this mainly contains the scenarios that I think are most important (in the sense that users would benefit from ty improving error messages in that area).

Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
# Invalid assignment diagnostics

<!-- snapshot-diagnostics -->

```toml
[environment]
python-version = "3.12"
```

This file contains various scenarios of `invalid-assignment` (and related) diagnostics where we
(attempt to) do better than just report "type X is not assignable to type Y".

## Basic

Mainly for comparison: this is the most basic kind of `invalid-assignment` diagnostic:

```py
def _(source: str):
target: bytes = source # error: [invalid-assignment]
```

## Unions

Assigning a union to a non-union:

```py
def _(source: str | None):
target: str = source # error: [invalid-assignment]
```

Assigning a non-union to a union:

```py
def _(source: int):
target: str | None = source # error: [invalid-assignment]
```

Assigning a union to a union:

```py
def _(source: str | None):
target: bytes | None = source # error: [invalid-assignment]
```

## Tuples

Wrong element types:

```py
def _(source: tuple[int, str, bool]):
target: tuple[int, bytes, bool] = source # error: [invalid-assignment]
```

Wrong number of elements:

```py
def _(source: tuple[int, str]):
target: tuple[int, str, bool] = source # error: [invalid-assignment]
```

## `Callable`

Assigning a function to a `Callable`

```py
from typing import Any, Callable

def source(x: int, y: str) -> None:
raise NotImplementedError

target: Callable[[int, bytes], bool] = source # error: [invalid-assignment]
```

Assigning a `Callable` to a `Callable` with wrong parameter type:

```py
def _(source: Callable[[int, str], bool]):
target: Callable[[int, bytes], bool] = source # error: [invalid-assignment]
```

Assigning a `Callable` to a `Callable` with wrong return type:

```py
def _(source: Callable[[int, bytes], None]):
target: Callable[[int, bytes], bool] = source # error: [invalid-assignment]
```

Assigning a `Callable` to a `Callable` with wrong number of parameters:

```py
def _(source: Callable[[int, str], bool]):
target: Callable[[int], bool] = source # error: [invalid-assignment]
```

Assigning a class to a `Callable`

```py
class Number:
def __init__(self, value: int): ...

target: Callable[[str], Any] = Number # error: [invalid-assignment]
```

## Function assignability and overrides

Liskov checks use function-to-function assignability.

Wrong parameter type:

```py
class Parent:
def method(self, x: str) -> bool:
raise NotImplementedError

class Child1(Parent):
# error: [invalid-method-override]
def method(self, x: bytes) -> bool:
raise NotImplementedError
```

Wrong return type:

```py
class Child2(Parent):
# error: [invalid-method-override]
def method(self, x: str) -> None:
raise NotImplementedError
```

Wrong non-positional-only parameter name:

```py
class Child3(Parent):
# error: [invalid-method-override]
def method(self, y: str):
raise NotImplementedError
```

## `TypedDict`

Incompatible field types:

```py
from typing import Any, TypedDict

class Person(TypedDict):
name: str

class Other(TypedDict):
name: bytes

def _(source: Person):
target: Other = source # error: [invalid-assignment]
```

Missing required fields:

```py
class PersonWithAge(TypedDict):
name: str
age: int

def _(source: Person):
target: PersonWithAge = source # error: [invalid-assignment]
```

Assigning a `TypedDict` to a `dict`

```py
class Person(TypedDict):
name: str

def _(source: Person):
target: dict[str, Any] = source # error: [invalid-assignment]
```

## Protocols

Missing protocol members:

```py
from typing import Protocol

class SupportsCheck(Protocol):
def check(self, x: int, y: str) -> bool: ...

class DoesNotHaveCheck: ...

def _(source: DoesNotHaveCheck):
target: SupportsCheck = source # error: [invalid-assignment]
```

Incompatible types for protocol members:

```py
class CheckWithWrongSignature:
def check(self, x: int, y: bytes) -> bool:
return False

def _(source: CheckWithWrongSignature):
target: SupportsCheck = source # error: [invalid-assignment]
```

## Type aliases

Type aliases should be expanded in diagnostics to understand the underlying incompatibilities:

```py
from typing import Protocol

class SupportsName(Protocol):
def name(self) -> str: ...

class HasName:
def name(self) -> bytes:
return b""

type StringOrName = str | SupportsName

def _(source: HasName):
target: SupportsName = source # error: [invalid-assignment]
```

## Deeply nested incompatibilities

```py
from typing import Callable

def source(x: tuple[int, str]) -> bool:
return False

target: Callable[[tuple[int, bytes]], bool] = source # error: [invalid-assignment]
```

## Multiple nested incompatibilities

```py
from typing import Protocol

class SupportsCheck(Protocol):
def check1(self, x: str): ...
def check2(self, x: int) -> bool: ...

class Incompatible:
def check1(self, x: bytes): ...
def check2(self, x: int) -> None: ...

def _(source: Incompatible):
target: SupportsCheck = source # error: [invalid-assignment]
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---

---
mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Basic
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md
---

# Python source files

## mdtest_snippet.py

```
1 | def _(source: str):
2 | target: bytes = source # error: [invalid-assignment]
```

# Diagnostics

```
error[invalid-assignment]: Object of type `str` is not assignable to `bytes`
--> src/mdtest_snippet.py:2:13
|
1 | def _(source: str):
2 | target: bytes = source # error: [invalid-assignment]
| ----- ^^^^^^ Incompatible value of type `str`
| |
| Declared type
|
info: rule `invalid-assignment` is enabled by default

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---

---
mdtest name: invalid_assignment_details.md - Invalid assignment diagnostics - Deeply nested incompatibilities
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_assignment_details.md
---

# Python source files

## mdtest_snippet.py

```
1 | from typing import Callable
2 |
3 | def source(x: tuple[int, str]) -> bool:
4 | return False
5 |
6 | target: Callable[[tuple[int, bytes]], bool] = source # error: [invalid-assignment]
```

# Diagnostics

```
error[invalid-assignment]: Object of type `def source(x: tuple[int, str]) -> bool` is not assignable to `(tuple[int, bytes], /) -> bool`
--> src/mdtest_snippet.py:6:9
|
4 | return False
5 |
6 | target: Callable[[tuple[int, bytes]], bool] = source # error: [invalid-assignment]
| ----------------------------------- ^^^^^^ Incompatible value of type `def source(x: tuple[int, str]) -> bool`
| |
| Declared type
|
info: rule `invalid-assignment` is enabled by default

```
Loading