[red-knot] Rewrite Type::try_iterate() to improve type inference and diagnostic messages (#16321)

This commit is contained in:
Alex Waygood 2025-02-25 14:02:03 +00:00 committed by GitHub
parent 1be0dc6885
commit 5c007db7e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 2107 additions and 162 deletions

View file

@ -105,7 +105,11 @@ reveal_type(x)
## With non-callable iterator ## With non-callable iterator
<!-- snapshot-diagnostics -->
```py ```py
from typing_extensions import reveal_type
def _(flag: bool): def _(flag: bool):
class NotIterable: class NotIterable:
if flag: if flag:
@ -113,7 +117,8 @@ def _(flag: bool):
else: else:
__iter__: None = None __iter__: None = None
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable" # error: [not-iterable]
for x in NotIterable():
pass pass
# revealed: Unknown # revealed: Unknown
@ -123,21 +128,25 @@ def _(flag: bool):
## Invalid iterable ## Invalid iterable
<!-- snapshot-diagnostics -->
```py ```py
nonsense = 123 nonsense = 123
for x in nonsense: # error: "Object of type `Literal[123]` is not iterable" for x in nonsense: # error: [not-iterable]
pass pass
``` ```
## New over old style iteration protocol ## New over old style iteration protocol
<!-- snapshot-diagnostics -->
```py ```py
class NotIterable: class NotIterable:
def __getitem__(self, key: int) -> int: def __getitem__(self, key: int) -> int:
return 42 return 42
__iter__: None = None __iter__: None = None
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable" for x in NotIterable(): # error: [not-iterable]
pass pass
``` ```
@ -221,7 +230,11 @@ def _(flag: bool):
## Union type as iterable where one union element has no `__iter__` method ## Union type as iterable where one union element has no `__iter__` method
<!-- snapshot-diagnostics -->
```py ```py
from typing_extensions import reveal_type
class TestIter: class TestIter:
def __next__(self) -> int: def __next__(self) -> int:
return 42 return 42
@ -231,14 +244,18 @@ class Test:
return TestIter() return TestIter()
def _(flag: bool): def _(flag: bool):
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound" # error: [not-iterable]
for x in Test() if flag else 42: for x in Test() if flag else 42:
reveal_type(x) # revealed: int reveal_type(x) # revealed: int
``` ```
## Union type as iterable where one union element has invalid `__iter__` method ## Union type as iterable where one union element has invalid `__iter__` method
<!-- snapshot-diagnostics -->
```py ```py
from typing_extensions import reveal_type
class TestIter: class TestIter:
def __next__(self) -> int: def __next__(self) -> int:
return 42 return 42
@ -253,7 +270,7 @@ class Test2:
def _(flag: bool): def _(flag: bool):
# TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989) # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
# error: "Object of type `Test | Test2` is not iterable" # error: [not-iterable]
for x in Test() if flag else Test2(): for x in Test() if flag else Test2():
reveal_type(x) # revealed: int reveal_type(x) # revealed: int
``` ```
@ -269,7 +286,454 @@ class Test:
def __iter__(self) -> TestIter | int: def __iter__(self) -> TestIter | int:
return TestIter() return TestIter()
# error: [not-iterable] "Object of type `Test` is not iterable" # error: [not-iterable] "Object of type `Test` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method"
for x in Test(): for x in Test():
reveal_type(x) # revealed: int reveal_type(x) # revealed: int
``` ```
## Possibly-not-callable `__iter__` method
```py
def _(flag: bool):
class Iterator:
def __next__(self) -> int:
return 42
class CustomCallable:
if flag:
def __call__(self, *args, **kwargs) -> Iterator:
return Iterator()
else:
__call__: None = None
class Iterable1:
__iter__: CustomCallable = CustomCallable()
class Iterable2:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
else:
__iter__: None = None
# error: [not-iterable] "Object of type `Iterable1` may not be iterable because its `__iter__` attribute (with type `CustomCallable`) may not be callable"
for x in Iterable1():
# TODO... `int` might be ideal here?
reveal_type(x) # revealed: int | Unknown
# error: [not-iterable] "Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `Literal[__iter__] | None`) may not be callable"
for y in Iterable2():
# TODO... `int` might be ideal here?
reveal_type(y) # revealed: int | Unknown
```
## `__iter__` method with a bad signature
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self, extra_arg) -> Iterator:
return Iterator()
# error: [not-iterable]
for x in Iterable():
reveal_type(x) # revealed: int
```
## `__iter__` does not return an iterator
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Bad:
def __iter__(self) -> int:
return 42
# error: [not-iterable]
for x in Bad():
reveal_type(x) # revealed: Unknown
```
## `__iter__` returns an object with a possibly unbound `__next__` method
```py
def _(flag: bool):
class Iterator:
if flag:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
# error: [not-iterable] "Object of type `Iterable` may not be iterable because its `__iter__` method returns an object of type `Iterator`, which may not have a `__next__` method"
for x in Iterable():
reveal_type(x) # revealed: int
```
## `__iter__` returns an iterator with an invalid `__next__` method
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Iterator1:
def __next__(self, extra_arg) -> int:
return 42
class Iterator2:
__next__: None = None
class Iterable1:
def __iter__(self) -> Iterator1:
return Iterator1()
class Iterable2:
def __iter__(self) -> Iterator2:
return Iterator2()
# error: [not-iterable]
for x in Iterable1():
reveal_type(x) # revealed: int
# error: [not-iterable]
for y in Iterable2():
reveal_type(y) # revealed: Unknown
```
## Possibly unbound `__iter__` and bad `__getitem__` method
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
def _(flag: bool):
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
# invalid signature because it only accepts a `str`,
# but the old-style iteration protocol will pass it an `int`
def __getitem__(self, key: str) -> bytes:
return 42
# error: [not-iterable]
for x in Iterable():
reveal_type(x) # revealed: int | bytes
```
## Possibly unbound `__iter__` and not-callable `__getitem__`
This snippet tests that we infer the element type correctly in the following edge case:
- `__iter__` is a method with the correct parameter spec that returns a valid iterator; BUT
- `__iter__` is possibly unbound; AND
- `__getitem__` is set to a non-callable type
It's important that we emit a diagnostic here, but it's also important that we still use the return
type of the iterator's `__next__` method as the inferred type of `x` in the `for` loop:
```py
def _(flag: bool):
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
__getitem__: None = None
# error: [not-iterable] "Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable"
for x in Iterable():
reveal_type(x) # revealed: int
```
## Possibly unbound `__iter__` and possibly unbound `__getitem__`
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Iterator:
def __next__(self) -> int:
return 42
def _(flag1: bool, flag2: bool):
class Iterable:
if flag1:
def __iter__(self) -> Iterator:
return Iterator()
if flag2:
def __getitem__(self, key: int) -> bytes:
return 42
# error: [not-iterable]
for x in Iterable():
reveal_type(x) # revealed: int | bytes
```
## No `__iter__` method and `__getitem__` is not callable
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Bad:
__getitem__: None = None
# error: [not-iterable]
for x in Bad():
reveal_type(x) # revealed: Unknown
```
## Possibly-not-callable `__getitem__` method
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
def _(flag: bool):
class CustomCallable:
if flag:
def __call__(self, *args, **kwargs) -> int:
return 42
else:
__call__: None = None
class Iterable1:
__getitem__: CustomCallable = CustomCallable()
class Iterable2:
if flag:
def __getitem__(self, key: int) -> int:
return 42
else:
__getitem__: None = None
# error: [not-iterable]
for x in Iterable1():
# TODO... `int` might be ideal here?
reveal_type(x) # revealed: int | Unknown
# error: [not-iterable]
for y in Iterable2():
# TODO... `int` might be ideal here?
reveal_type(y) # revealed: int | Unknown
```
## Bad `__getitem__` method
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Iterable:
# invalid because it will implicitly be passed an `int`
# by the interpreter
def __getitem__(self, key: str) -> int:
return 42
# error: [not-iterable]
for x in Iterable():
reveal_type(x) # revealed: int
```
## Possibly unbound `__iter__` but definitely bound `__getitem__`
Here, we should not emit a diagnostic: if `__iter__` is unbound, we should fallback to
`__getitem__`:
```py
class Iterator:
def __next__(self) -> str:
return "foo"
def _(flag: bool):
class Iterable:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
def __getitem__(self, key: int) -> bytes:
return b"foo"
for x in Iterable():
reveal_type(x) # revealed: str | bytes
```
## Possibly invalid `__iter__` methods
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Iterator:
def __next__(self) -> int:
return 42
def _(flag: bool):
class Iterable1:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
else:
def __iter__(self, invalid_extra_arg) -> Iterator:
return Iterator()
# error: [not-iterable]
for x in Iterable1():
reveal_type(x) # revealed: int
class Iterable2:
if flag:
def __iter__(self) -> Iterator:
return Iterator()
else:
__iter__: None = None
# error: [not-iterable]
for x in Iterable2():
# TODO: `int` would probably be better here:
reveal_type(x) # revealed: int | Unknown
```
## Possibly invalid `__next__` method
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
def _(flag: bool):
class Iterator1:
if flag:
def __next__(self) -> int:
return 42
else:
def __next__(self, invalid_extra_arg) -> str:
return "foo"
class Iterator2:
if flag:
def __next__(self) -> int:
return 42
else:
__next__: None = None
class Iterable1:
def __iter__(self) -> Iterator1:
return Iterator1()
class Iterable2:
def __iter__(self) -> Iterator2:
return Iterator2()
# error: [not-iterable]
for x in Iterable1():
reveal_type(x) # revealed: int | str
# error: [not-iterable]
for y in Iterable2():
# TODO: `int` would probably be better here:
reveal_type(y) # revealed: int | Unknown
```
## Possibly invalid `__getitem__` methods
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
def _(flag: bool):
class Iterable1:
if flag:
def __getitem__(self, item: int) -> str:
return "foo"
else:
__getitem__: None = None
class Iterable2:
if flag:
def __getitem__(self, item: int) -> str:
return "foo"
else:
def __getitem__(self, item: str) -> int:
return "foo"
# error: [not-iterable]
for x in Iterable1():
# TODO: `str` might be better
reveal_type(x) # revealed: str | Unknown
# error: [not-iterable]
for y in Iterable2():
reveal_type(y) # revealed: str | int
```
## Possibly unbound `__iter__` and possibly invalid `__getitem__`
<!-- snapshot-diagnostics -->
```py
from typing_extensions import reveal_type
class Iterator:
def __next__(self) -> bytes:
return b"foo"
def _(flag: bool, flag2: bool):
class Iterable1:
if flag:
def __getitem__(self, item: int) -> str:
return "foo"
else:
__getitem__: None = None
if flag2:
def __iter__(self) -> Iterator:
return Iterator()
class Iterable2:
if flag:
def __getitem__(self, item: int) -> str:
return "foo"
else:
def __getitem__(self, item: str) -> int:
return "foo"
if flag2:
def __iter__(self) -> Iterator:
return Iterator()
# error: [not-iterable]
for x in Iterable1():
# TODO: `bytes | str` might be better
reveal_type(x) # revealed: bytes | str | Unknown
# error: [not-iterable]
for y in Iterable2():
reveal_type(y) # revealed: bytes | str | int
```

View file

@ -0,0 +1,52 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Bad `__getitem__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Iterable:
4 | # invalid because it will implicitly be passed an `int`
5 | # by the interpreter
6 | def __getitem__(self, key: str) -> int:
7 | return 42
8 |
9 | # error: [not-iterable]
10 | for x in Iterable():
11 | reveal_type(x) # revealed: int
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:10:10
|
9 | # error: [not-iterable]
10 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because it has no `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
11 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:11:5
|
9 | # error: [not-iterable]
10 | for x in Iterable():
11 | reveal_type(x) # revealed: int
| -------------- info: Revealed type is `int`
|
```

View file

@ -0,0 +1,32 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Invalid iterable
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | nonsense = 123
2 | for x in nonsense: # error: [not-iterable]
3 | pass
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:2:10
|
1 | nonsense = 123
2 | for x in nonsense: # error: [not-iterable]
| ^^^^^^^^ Object of type `Literal[123]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
3 | pass
|
```

View file

@ -0,0 +1,37 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - New over old style iteration protocol
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | class NotIterable:
2 | def __getitem__(self, key: int) -> int:
3 | return 42
4 | __iter__: None = None
5 |
6 | for x in NotIterable(): # error: [not-iterable]
7 | pass
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:6:10
|
4 | __iter__: None = None
5 |
6 | for x in NotIterable(): # error: [not-iterable]
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `None`, which is not callable
7 | pass
|
```

View file

@ -0,0 +1,49 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - No `__iter__` method and `__getitem__` is not callable
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Bad:
4 | __getitem__: None = None
5 |
6 | # error: [not-iterable]
7 | for x in Bad():
8 | reveal_type(x) # revealed: Unknown
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:7:10
|
6 | # error: [not-iterable]
7 | for x in Bad():
| ^^^^^ Object of type `Bad` is not iterable because it has no `__iter__` method and its `__getitem__` attribute has type `None`, which is not callable
8 | reveal_type(x) # revealed: Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:8:5
|
6 | # error: [not-iterable]
7 | for x in Bad():
8 | reveal_type(x) # revealed: Unknown
| -------------- info: Revealed type is `Unknown`
|
```

View file

@ -0,0 +1,98 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly-not-callable `__getitem__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def _(flag: bool):
4 | class CustomCallable:
5 | if flag:
6 | def __call__(self, *args, **kwargs) -> int:
7 | return 42
8 | else:
9 | __call__: None = None
10 |
11 | class Iterable1:
12 | __getitem__: CustomCallable = CustomCallable()
13 |
14 | class Iterable2:
15 | if flag:
16 | def __getitem__(self, key: int) -> int:
17 | return 42
18 | else:
19 | __getitem__: None = None
20 |
21 | # error: [not-iterable]
22 | for x in Iterable1():
23 | # TODO... `int` might be ideal here?
24 | reveal_type(x) # revealed: int | Unknown
25 |
26 | # error: [not-iterable]
27 | for y in Iterable2():
28 | # TODO... `int` might be ideal here?
29 | reveal_type(y) # revealed: int | Unknown
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:22:14
|
21 | # error: [not-iterable]
22 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `CustomCallable`) may not be callable
23 | # TODO... `int` might be ideal here?
24 | reveal_type(x) # revealed: int | Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:24:9
|
22 | for x in Iterable1():
23 | # TODO... `int` might be ideal here?
24 | reveal_type(x) # revealed: int | Unknown
| -------------- info: Revealed type is `int | Unknown`
25 |
26 | # error: [not-iterable]
|
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:27:14
|
26 | # error: [not-iterable]
27 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `Literal[__getitem__] | None`) may not be callable
28 | # TODO... `int` might be ideal here?
29 | reveal_type(y) # revealed: int | Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:29:9
|
27 | for y in Iterable2():
28 | # TODO... `int` might be ideal here?
29 | reveal_type(y) # revealed: int | Unknown
| -------------- info: Revealed type is `int | Unknown`
|
```

View file

@ -0,0 +1,94 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly invalid `__getitem__` methods
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def _(flag: bool):
4 | class Iterable1:
5 | if flag:
6 | def __getitem__(self, item: int) -> str:
7 | return "foo"
8 | else:
9 | __getitem__: None = None
10 |
11 | class Iterable2:
12 | if flag:
13 | def __getitem__(self, item: int) -> str:
14 | return "foo"
15 | else:
16 | def __getitem__(self, item: str) -> int:
17 | return "foo"
18 |
19 | # error: [not-iterable]
20 | for x in Iterable1():
21 | # TODO: `str` might be better
22 | reveal_type(x) # revealed: str | Unknown
23 |
24 | # error: [not-iterable]
25 | for y in Iterable2():
26 | reveal_type(y) # revealed: str | int
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:20:14
|
19 | # error: [not-iterable]
20 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it has no `__iter__` method and its `__getitem__` attribute (with type `Literal[__getitem__] | None`) may not be callable
21 | # TODO: `str` might be better
22 | reveal_type(x) # revealed: str | Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:22:9
|
20 | for x in Iterable1():
21 | # TODO: `str` might be better
22 | reveal_type(x) # revealed: str | Unknown
| -------------- info: Revealed type is `str | Unknown`
23 |
24 | # error: [not-iterable]
|
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:25:14
|
24 | # error: [not-iterable]
25 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it has no `__iter__` method and its `__getitem__` method (with type `Literal[__getitem__, __getitem__]`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
26 | reveal_type(y) # revealed: str | int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:26:9
|
24 | # error: [not-iterable]
25 | for y in Iterable2():
26 | reveal_type(y) # revealed: str | int
| -------------- info: Revealed type is `str | int`
|
```

View file

@ -0,0 +1,98 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly invalid `__iter__` methods
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Iterator:
4 | def __next__(self) -> int:
5 | return 42
6 |
7 | def _(flag: bool):
8 | class Iterable1:
9 | if flag:
10 | def __iter__(self) -> Iterator:
11 | return Iterator()
12 | else:
13 | def __iter__(self, invalid_extra_arg) -> Iterator:
14 | return Iterator()
15 |
16 | # error: [not-iterable]
17 | for x in Iterable1():
18 | reveal_type(x) # revealed: int
19 |
20 | class Iterable2:
21 | if flag:
22 | def __iter__(self) -> Iterator:
23 | return Iterator()
24 | else:
25 | __iter__: None = None
26 |
27 | # error: [not-iterable]
28 | for x in Iterable2():
29 | # TODO: `int` would probably be better here:
30 | reveal_type(x) # revealed: int | Unknown
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:17:14
|
16 | # error: [not-iterable]
17 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method (with type `Literal[__iter__, __iter__]`) may have an invalid signature (expected `def __iter__(self): ...`)
18 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:18:9
|
16 | # error: [not-iterable]
17 | for x in Iterable1():
18 | reveal_type(x) # revealed: int
| -------------- info: Revealed type is `int`
19 |
20 | class Iterable2:
|
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:28:14
|
27 | # error: [not-iterable]
28 | for x in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` attribute (with type `Literal[__iter__] | None`) may not be callable
29 | # TODO: `int` would probably be better here:
30 | reveal_type(x) # revealed: int | Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:30:9
|
28 | for x in Iterable2():
29 | # TODO: `int` would probably be better here:
30 | reveal_type(x) # revealed: int | Unknown
| -------------- info: Revealed type is `int | Unknown`
|
```

View file

@ -0,0 +1,102 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly invalid `__next__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def _(flag: bool):
4 | class Iterator1:
5 | if flag:
6 | def __next__(self) -> int:
7 | return 42
8 | else:
9 | def __next__(self, invalid_extra_arg) -> str:
10 | return "foo"
11 |
12 | class Iterator2:
13 | if flag:
14 | def __next__(self) -> int:
15 | return 42
16 | else:
17 | __next__: None = None
18 |
19 | class Iterable1:
20 | def __iter__(self) -> Iterator1:
21 | return Iterator1()
22 |
23 | class Iterable2:
24 | def __iter__(self) -> Iterator2:
25 | return Iterator2()
26 |
27 | # error: [not-iterable]
28 | for x in Iterable1():
29 | reveal_type(x) # revealed: int | str
30 |
31 | # error: [not-iterable]
32 | for y in Iterable2():
33 | # TODO: `int` would probably be better here:
34 | reveal_type(y) # revealed: int | Unknown
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:28:14
|
27 | # error: [not-iterable]
28 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method (expected `def __next__(self): ...`)
29 | reveal_type(x) # revealed: int | str
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:29:9
|
27 | # error: [not-iterable]
28 | for x in Iterable1():
29 | reveal_type(x) # revealed: int | str
| -------------- info: Revealed type is `int | str`
30 |
31 | # error: [not-iterable]
|
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:32:14
|
31 | # error: [not-iterable]
32 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that may not be callable
33 | # TODO: `int` would probably be better here:
34 | reveal_type(y) # revealed: int | Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:34:9
|
32 | for y in Iterable2():
33 | # TODO: `int` would probably be better here:
34 | reveal_type(y) # revealed: int | Unknown
| -------------- info: Revealed type is `int | Unknown`
|
```

View file

@ -0,0 +1,60 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly unbound `__iter__` and bad `__getitem__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def _(flag: bool):
4 | class Iterator:
5 | def __next__(self) -> int:
6 | return 42
7 |
8 | class Iterable:
9 | if flag:
10 | def __iter__(self) -> Iterator:
11 | return Iterator()
12 | # invalid signature because it only accepts a `str`,
13 | # but the old-style iteration protocol will pass it an `int`
14 | def __getitem__(self, key: str) -> bytes:
15 | return 42
16 |
17 | # error: [not-iterable]
18 | for x in Iterable():
19 | reveal_type(x) # revealed: int | bytes
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:18:14
|
17 | # error: [not-iterable]
18 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method has an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
19 | reveal_type(x) # revealed: int | bytes
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:19:9
|
17 | # error: [not-iterable]
18 | for x in Iterable():
19 | reveal_type(x) # revealed: int | bytes
| -------------- info: Revealed type is `int | bytes`
|
```

View file

@ -0,0 +1,106 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly invalid `__getitem__`
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Iterator:
4 | def __next__(self) -> bytes:
5 | return b"foo"
6 |
7 | def _(flag: bool, flag2: bool):
8 | class Iterable1:
9 | if flag:
10 | def __getitem__(self, item: int) -> str:
11 | return "foo"
12 | else:
13 | __getitem__: None = None
14 |
15 | if flag2:
16 | def __iter__(self) -> Iterator:
17 | return Iterator()
18 |
19 | class Iterable2:
20 | if flag:
21 | def __getitem__(self, item: int) -> str:
22 | return "foo"
23 | else:
24 | def __getitem__(self, item: str) -> int:
25 | return "foo"
26 | if flag2:
27 | def __iter__(self) -> Iterator:
28 | return Iterator()
29 |
30 | # error: [not-iterable]
31 | for x in Iterable1():
32 | # TODO: `bytes | str` might be better
33 | reveal_type(x) # revealed: bytes | str | Unknown
34 |
35 | # error: [not-iterable]
36 | for y in Iterable2():
37 | reveal_type(y) # revealed: bytes | str | int
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:31:14
|
30 | # error: [not-iterable]
31 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` may not be iterable because it may not have an `__iter__` method and its `__getitem__` attribute (with type `Literal[__getitem__] | None`) may not be callable
32 | # TODO: `bytes | str` might be better
33 | reveal_type(x) # revealed: bytes | str | Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:33:9
|
31 | for x in Iterable1():
32 | # TODO: `bytes | str` might be better
33 | reveal_type(x) # revealed: bytes | str | Unknown
| -------------- info: Revealed type is `bytes | str | Unknown`
34 |
35 | # error: [not-iterable]
|
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:36:14
|
35 | # error: [not-iterable]
36 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `Literal[__getitem__, __getitem__]`)
may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
37 | reveal_type(y) # revealed: bytes | str | int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:37:9
|
35 | # error: [not-iterable]
36 | for y in Iterable2():
37 | reveal_type(y) # revealed: bytes | str | int
| -------------- info: Revealed type is `bytes | str | int`
|
```

View file

@ -0,0 +1,59 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Possibly unbound `__iter__` and possibly unbound `__getitem__`
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Iterator:
4 | def __next__(self) -> int:
5 | return 42
6 |
7 | def _(flag1: bool, flag2: bool):
8 | class Iterable:
9 | if flag1:
10 | def __iter__(self) -> Iterator:
11 | return Iterator()
12 | if flag2:
13 | def __getitem__(self, key: int) -> bytes:
14 | return 42
15 |
16 | # error: [not-iterable]
17 | for x in Iterable():
18 | reveal_type(x) # revealed: int | bytes
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:17:14
|
16 | # error: [not-iterable]
17 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` may not be iterable because it may not have an `__iter__` method or a `__getitem__` method
18 | reveal_type(x) # revealed: int | bytes
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:18:9
|
16 | # error: [not-iterable]
17 | for x in Iterable():
18 | reveal_type(x) # revealed: int | bytes
| -------------- info: Revealed type is `int | bytes`
|
```

View file

@ -0,0 +1,61 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Union type as iterable where one union element has invalid `__iter__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class TestIter:
4 | def __next__(self) -> int:
5 | return 42
6 |
7 | class Test:
8 | def __iter__(self) -> TestIter:
9 | return TestIter()
10 |
11 | class Test2:
12 | def __iter__(self) -> int:
13 | return 42
14 |
15 | def _(flag: bool):
16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
17 | # error: [not-iterable]
18 | for x in Test() if flag else Test2():
19 | reveal_type(x) # revealed: int
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:18:14
|
16 | # TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
17 | # error: [not-iterable]
18 | for x in Test() if flag else Test2():
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Test2` may not be iterable because its `__iter__` method returns an object of type `TestIter | int`, which may not have a `__next__` method
19 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:19:9
|
17 | # error: [not-iterable]
18 | for x in Test() if flag else Test2():
19 | reveal_type(x) # revealed: int
| -------------- info: Revealed type is `int`
|
```

View file

@ -0,0 +1,56 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - Union type as iterable where one union element has no `__iter__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class TestIter:
4 | def __next__(self) -> int:
5 | return 42
6 |
7 | class Test:
8 | def __iter__(self) -> TestIter:
9 | return TestIter()
10 |
11 | def _(flag: bool):
12 | # error: [not-iterable]
13 | for x in Test() if flag else 42:
14 | reveal_type(x) # revealed: int
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:13:14
|
11 | def _(flag: bool):
12 | # error: [not-iterable]
13 | for x in Test() if flag else 42:
| ^^^^^^^^^^^^^^^^^^^^^^ Object of type `Test | Literal[42]` may not be iterable because it may not have an `__iter__` method and it doesn't have a `__getitem__` method
14 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:14:9
|
12 | # error: [not-iterable]
13 | for x in Test() if flag else 42:
14 | reveal_type(x) # revealed: int
| -------------- info: Revealed type is `int`
|
```

View file

@ -0,0 +1,69 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - With non-callable iterator
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | def _(flag: bool):
4 | class NotIterable:
5 | if flag:
6 | __iter__: int = 1
7 | else:
8 | __iter__: None = None
9 |
10 | # error: [not-iterable]
11 | for x in NotIterable():
12 | pass
13 |
14 | # revealed: Unknown
15 | # error: [possibly-unresolved-reference]
16 | reveal_type(x)
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:11:14
|
10 | # error: [not-iterable]
11 | for x in NotIterable():
| ^^^^^^^^^^^^^ Object of type `NotIterable` is not iterable because its `__iter__` attribute has type `int | None`, which is not callable
12 | pass
|
```
```
warning: lint:possibly-unresolved-reference
--> /src/mdtest_snippet.py:16:17
|
14 | # revealed: Unknown
15 | # error: [possibly-unresolved-reference]
16 | reveal_type(x)
| - Name `x` used when possibly not defined
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:16:5
|
14 | # revealed: Unknown
15 | # error: [possibly-unresolved-reference]
16 | reveal_type(x)
| -------------- info: Revealed type is `Unknown`
|
```

View file

@ -0,0 +1,50 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - `__iter__` does not return an iterator
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Bad:
4 | def __iter__(self) -> int:
5 | return 42
6 |
7 | # error: [not-iterable]
8 | for x in Bad():
9 | reveal_type(x) # revealed: Unknown
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:8:10
|
7 | # error: [not-iterable]
8 | for x in Bad():
| ^^^^^ Object of type `Bad` is not iterable because its `__iter__` method returns an object of type `int`, which has no `__next__` method
9 | reveal_type(x) # revealed: Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:9:5
|
7 | # error: [not-iterable]
8 | for x in Bad():
9 | reveal_type(x) # revealed: Unknown
| -------------- info: Revealed type is `Unknown`
|
```

View file

@ -0,0 +1,54 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - `__iter__` method with a bad signature
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Iterator:
4 | def __next__(self) -> int:
5 | return 42
6 |
7 | class Iterable:
8 | def __iter__(self, extra_arg) -> Iterator:
9 | return Iterator()
10 |
11 | # error: [not-iterable]
12 | for x in Iterable():
13 | reveal_type(x) # revealed: int
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:12:10
|
11 | # error: [not-iterable]
12 | for x in Iterable():
| ^^^^^^^^^^ Object of type `Iterable` is not iterable because its `__iter__` method has an invalid signature (expected `def __iter__(self): ...`)
13 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:13:5
|
11 | # error: [not-iterable]
12 | for x in Iterable():
13 | reveal_type(x) # revealed: int
| -------------- info: Revealed type is `int`
|
```

View file

@ -0,0 +1,91 @@
---
source: crates/red_knot_test/src/lib.rs
expression: snapshot
---
---
mdtest name: for.md - For loops - `__iter__` returns an iterator with an invalid `__next__` method
mdtest path: crates/red_knot_python_semantic/resources/mdtest/loops/for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class Iterator1:
4 | def __next__(self, extra_arg) -> int:
5 | return 42
6 |
7 | class Iterator2:
8 | __next__: None = None
9 |
10 | class Iterable1:
11 | def __iter__(self) -> Iterator1:
12 | return Iterator1()
13 |
14 | class Iterable2:
15 | def __iter__(self) -> Iterator2:
16 | return Iterator2()
17 |
18 | # error: [not-iterable]
19 | for x in Iterable1():
20 | reveal_type(x) # revealed: int
21 |
22 | # error: [not-iterable]
23 | for y in Iterable2():
24 | reveal_type(y) # revealed: Unknown
```
# Diagnostics
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:19:10
|
18 | # error: [not-iterable]
19 | for x in Iterable1():
| ^^^^^^^^^^^ Object of type `Iterable1` is not iterable because its `__iter__` method returns an object of type `Iterator1`, which has an invalid `__next__` method (expected `def __next__(self): ...`)
20 | reveal_type(x) # revealed: int
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:20:5
|
18 | # error: [not-iterable]
19 | for x in Iterable1():
20 | reveal_type(x) # revealed: int
| -------------- info: Revealed type is `int`
21 |
22 | # error: [not-iterable]
|
```
```
error: lint:not-iterable
--> /src/mdtest_snippet.py:23:10
|
22 | # error: [not-iterable]
23 | for y in Iterable2():
| ^^^^^^^^^^^ Object of type `Iterable2` is not iterable because its `__iter__` method returns an object of type `Iterator2`, which has a `__next__` attribute that is not callable
24 | reveal_type(y) # revealed: Unknown
|
```
```
info: revealed-type
--> /src/mdtest_snippet.py:24:5
|
22 | # error: [not-iterable]
23 | for y in Iterable2():
24 | reveal_type(y) # revealed: Unknown
| -------------- info: Revealed type is `Unknown`
|
```

View file

@ -22,7 +22,7 @@ error: lint:not-iterable
--> /src/mdtest_snippet.py:1:8 --> /src/mdtest_snippet.py:1:8
| |
1 | a, b = 1 # error: [not-iterable] 1 | a, b = 1 # error: [not-iterable]
| ^ Object of type `Literal[1]` is not iterable | ^ Object of type `Literal[1]` is not iterable because it doesn't have an `__iter__` method or a `__getitem__` method
| |
``` ```

View file

@ -4,7 +4,7 @@ use std::str::FromStr;
use bitflags::bitflags; use bitflags::bitflags;
use call::{CallDunderError, CallError}; use call::{CallDunderError, CallError};
use context::InferContext; use context::InferContext;
use diagnostic::{report_not_iterable, report_not_iterable_possibly_unbound}; use diagnostic::NOT_ITERABLE;
use ruff_db::files::File; use ruff_db::files::File;
use ruff_python_ast as ast; use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
@ -1737,7 +1737,7 @@ impl<'db> Type<'db> {
// it still results in loosing information. Or should the information // it still results in loosing information. Or should the information
// be recomputed when rendering the diagnostic? // be recomputed when rendering the diagnostic?
CallError::Union(union_error) => { CallError::Union(union_error) => {
if let Type::Union(_) = union_error.called_ty { if let Type::Union(_) = union_error.called_type {
if union_error.errors.len() == 1 { if union_error.errors.len() == 1 {
union_error.errors.into_vec().pop().unwrap() union_error.errors.into_vec().pop().unwrap()
} else { } else {
@ -2237,15 +2237,15 @@ impl<'db> Type<'db> {
// Turn "`<type of illegal '__call__'>` not callable" into // Turn "`<type of illegal '__call__'>` not callable" into
// "`X` not callable" // "`X` not callable"
CallError::NotCallable { CallError::NotCallable {
not_callable_ty: self, not_callable_type: self,
} }
} }
CallDunderError::Call(CallError::Union(UnionCallError { CallDunderError::Call(CallError::Union(UnionCallError {
called_ty: _, called_type: _,
bindings, bindings,
errors, errors,
})) => CallError::Union(UnionCallError { })) => CallError::Union(UnionCallError {
called_ty: self, called_type: self,
bindings, bindings,
errors, errors,
}), }),
@ -2261,7 +2261,7 @@ impl<'db> Type<'db> {
CallDunderError::MethodNotAvailable => { CallDunderError::MethodNotAvailable => {
// Turn "`X.__call__` unbound" into "`X` not callable" // Turn "`X.__call__` unbound" into "`X` not callable"
CallError::NotCallable { CallError::NotCallable {
not_callable_ty: self, not_callable_type: self,
} }
} }
}) })
@ -2279,7 +2279,7 @@ impl<'db> Type<'db> {
))), ))),
_ => Err(CallError::NotCallable { _ => Err(CallError::NotCallable {
not_callable_ty: self, not_callable_type: self,
}), }),
} }
} }
@ -2317,7 +2317,7 @@ impl<'db> Type<'db> {
Type::Dynamic(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(self))), Type::Dynamic(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(self))),
_ => Err(CallError::NotCallable { _ => Err(CallError::NotCallable {
not_callable_ty: self, not_callable_type: self,
}), }),
} }
} }
@ -2350,7 +2350,7 @@ impl<'db> Type<'db> {
/// For type checking, use [`try_iterate`](Self::try_iterate) instead. /// For type checking, use [`try_iterate`](Self::try_iterate) instead.
fn iterate(self, db: &'db dyn Db) -> Type<'db> { fn iterate(self, db: &'db dyn Db) -> Type<'db> {
self.try_iterate(db) self.try_iterate(db)
.unwrap_or_else(|err| err.fallback_element_type()) .unwrap_or_else(|err| err.fallback_element_type(db))
} }
/// Given the type of an object that is iterated over in some way, /// Given the type of an object that is iterated over in some way,
@ -2361,75 +2361,98 @@ impl<'db> Type<'db> {
/// for y in x: /// for y in x:
/// pass /// pass
/// ``` /// ```
fn try_iterate(self, db: &'db dyn Db) -> Result<Type<'db>, IterateError<'db>> { fn try_iterate(self, db: &'db dyn Db) -> Result<Type<'db>, IterationError<'db>> {
if let Type::Tuple(tuple_type) = self { if let Type::Tuple(tuple_type) = self {
return Ok(UnionType::from_elements(db, tuple_type.elements(db))); return Ok(UnionType::from_elements(db, tuple_type.elements(db)));
} }
let dunder_iter_result = self.try_call_dunder(db, "__iter__", &CallArguments::none()); let try_call_dunder_getitem = || {
match &dunder_iter_result { self.try_call_dunder(
Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => {
let iterator_ty = outcome.return_type(db);
return match iterator_ty.try_call_dunder(db, "__next__", &CallArguments::none()) {
Ok(outcome) => {
if matches!(
dunder_iter_result,
Err(CallDunderError::PossiblyUnbound { .. })
) {
Err(IterateError::PossiblyUnbound {
iterable_ty: self,
element_ty: outcome.return_type(db),
})
} else {
Ok(outcome.return_type(db))
}
}
Err(CallDunderError::PossiblyUnbound(outcome)) => {
Err(IterateError::PossiblyUnbound {
iterable_ty: self,
element_ty: outcome.return_type(db),
})
}
Err(_) => Err(IterateError::NotIterable {
not_iterable_ty: self,
}),
};
}
// If `__iter__` exists but can't be called or doesn't have the expected signature,
// return not iterable over falling back to `__getitem__`.
Err(CallDunderError::Call(_)) => {
return Err(IterateError::NotIterable {
not_iterable_ty: self,
})
}
Err(CallDunderError::MethodNotAvailable) => {
// No `__iter__` attribute, try `__getitem__` next.
}
}
// Although it's not considered great practice,
// classes that define `__getitem__` are also iterable,
// even if they do not define `__iter__`.
//
// TODO(Alex) this is only valid if the `__getitem__` method is annotated as
// accepting `int` or `SupportsIndex`
match self.try_call_dunder(
db, db,
"__getitem__", "__getitem__",
&CallArguments::positional([KnownClass::Int.to_instance(db)]), &CallArguments::positional([KnownClass::Int.to_instance(db)]),
) { )
Ok(outcome) => Ok(outcome.return_type(db)), .map(|dunder_getitem_outcome| dunder_getitem_outcome.return_type(db))
Err(CallDunderError::PossiblyUnbound(outcome)) => Err(IterateError::PossiblyUnbound { };
iterable_ty: self,
element_ty: outcome.return_type(db), let try_call_dunder_next_on_iterator = |iterator: Type<'db>| {
}), iterator
Err(_) => Err(IterateError::NotIterable { .try_call_dunder(db, "__next__", &CallArguments::none())
not_iterable_ty: self, .map(|dunder_next_outcome| dunder_next_outcome.return_type(db))
};
let dunder_iter_result = self
.try_call_dunder(db, "__iter__", &CallArguments::none())
.map(|dunder_iter_outcome| dunder_iter_outcome.return_type(db));
let iteration_result = match dunder_iter_result {
Ok(iterator) => {
// `__iter__` is definitely bound and calling it succeeds.
// See what calling `__next__` on the object returned by `__iter__` gives us...
try_call_dunder_next_on_iterator(iterator).map_err(|dunder_next_error| {
IterationErrorKind::IterReturnsInvalidIterator {
iterator,
dunder_next_error,
}
})
}
// `__iter__` is possibly unbound...
Err(CallDunderError::PossiblyUnbound(dunder_iter_outcome)) => {
let iterator = dunder_iter_outcome.return_type(db);
match try_call_dunder_next_on_iterator(iterator) {
Ok(dunder_next_return) => {
try_call_dunder_getitem()
.map(|dunder_getitem_return_type| {
// If `__iter__` is possibly unbound,
// but it returns an object that has a bound and valid `__next__` method,
// *and* the object has a bound and valid `__getitem__` method,
// we infer a union of the type returned by the `__next__` method
// and the type returned by the `__getitem__` method.
//
// No diagnostic is emitted; iteration will always succeed!
UnionType::from_elements(
db,
[dunder_next_return, dunder_getitem_return_type],
)
})
.map_err(|dunder_getitem_error| {
IterationErrorKind::PossiblyUnboundIterAndGetitemError {
dunder_next_return,
dunder_getitem_error,
}
})
}
Err(dunder_next_error) => Err(IterationErrorKind::IterReturnsInvalidIterator {
iterator,
dunder_next_error,
}), }),
} }
} }
// `__iter__` is definitely bound but it can't be called with the expected arguments
Err(CallDunderError::Call(dunder_iter_call_error)) => {
Err(IterationErrorKind::IterCallError(dunder_iter_call_error))
}
// There's no `__iter__` method. Try `__getitem__` instead...
Err(CallDunderError::MethodNotAvailable) => {
try_call_dunder_getitem().map_err(|dunder_getitem_error| {
IterationErrorKind::UnboundIterAndGetitemError {
dunder_getitem_error,
}
})
}
};
iteration_result.map_err(|error_kind| IterationError {
iterable_type: self,
error_kind,
})
}
#[must_use] #[must_use]
pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> { pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> {
match self { match self {
@ -2909,51 +2932,348 @@ pub enum TypeVarBoundOrConstraints<'db> {
Constraints(TupleType<'db>), Constraints(TupleType<'db>),
} }
/// Error returned if a type isn't iterable. /// Error returned if a type is not (or may not be) iterable.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug)]
enum IterateError<'db> { struct IterationError<'db> {
/// The type isn't iterable because it doesn't implement the new-style or old-style iteration protocol /// The type of the object that the analysed code attempted to iterate over.
/// iterable_type: Type<'db>,
/// The new-style iteration protocol requires a type being iterated over to have an `__iter__`
/// method that returns something with a `__next__` method. The old-style iteration
/// protocol requires a type being iterated over to have a `__getitem__` method that accepts
/// a positive-integer argument.
NotIterable { not_iterable_ty: Type<'db> },
/// The type is iterable but the methods aren't always bound. /// The precise kind of error encountered when trying to iterate over the type.
PossiblyUnbound { error_kind: IterationErrorKind<'db>,
iterable_ty: Type<'db>,
element_ty: Type<'db>,
},
}
impl<'db> IterateError<'db> {
/// Reports the diagnostic for this error.
fn report_diagnostic(&self, context: &InferContext<'db>, iterable_node: ast::AnyNodeRef) {
match self {
Self::NotIterable { not_iterable_ty } => {
report_not_iterable(context, iterable_node, *not_iterable_ty);
}
Self::PossiblyUnbound {
iterable_ty,
element_ty: _,
} => {
report_not_iterable_possibly_unbound(context, iterable_node, *iterable_ty);
}
}
} }
impl<'db> IterationError<'db> {
/// Returns the element type if it is known, or `None` if the type is never iterable. /// Returns the element type if it is known, or `None` if the type is never iterable.
fn element_type(&self) -> Option<Type<'db>> { fn element_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self { self.error_kind.element_type(db)
IterateError::NotIterable { .. } => None,
IterateError::PossiblyUnbound { element_ty, .. } => Some(*element_ty),
}
} }
/// Returns the element type if it is known, or `Type::unknown()` if it is not. /// Returns the element type if it is known, or `Type::unknown()` if it is not.
fn fallback_element_type(&self) -> Type<'db> { fn fallback_element_type(&self, db: &'db dyn Db) -> Type<'db> {
self.element_type().unwrap_or(Type::unknown()) self.element_type(db).unwrap_or(Type::unknown())
}
/// Reports the diagnostic for this error.
fn report_diagnostic(&self, context: &InferContext<'db>, iterable_node: ast::AnyNodeRef) {
self.error_kind
.report_diagnostic(context, self.iterable_type, iterable_node);
}
}
#[derive(Debug)]
enum IterationErrorKind<'db> {
/// The object being iterated over has a bound `__iter__` method,
/// but calling it with the expected arguments results in an error.
IterCallError(CallError<'db>),
/// The object being iterated over has a bound `__iter__` method that can be called
/// with the expected types, but it returns an object that is not a valid iterator.
IterReturnsInvalidIterator {
/// The type of the object returned by the `__iter__` method.
iterator: Type<'db>,
/// The error we encountered when we tried to call `__next__` on the type
/// returned by `__iter__`
dunder_next_error: CallDunderError<'db>,
},
/// The object being iterated over has a bound `__iter__` method that returns a
/// valid iterator. However, the `__iter__` method is possibly unbound, and there
/// either isn't a `__getitem__` method to fall back to, or calling the `__getitem__`
/// method returns some kind of error.
PossiblyUnboundIterAndGetitemError {
/// The type of the object returned by the `__next__` method on the iterator.
/// (The iterator being the type returned by the `__iter__` method on the iterable.)
dunder_next_return: Type<'db>,
/// The error we encountered when we tried to call `__getitem__` on the iterable.
dunder_getitem_error: CallDunderError<'db>,
},
/// The object being iterated over doesn't have an `__iter__` method.
/// It also either doesn't have a `__getitem__` method to fall back to,
/// or calling the `__getitem__` method returns some kind of error.
UnboundIterAndGetitemError {
dunder_getitem_error: CallDunderError<'db>,
},
}
impl<'db> IterationErrorKind<'db> {
/// Returns the element type if it is known, or `None` if the type is never iterable.
fn element_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::IterReturnsInvalidIterator {
dunder_next_error, ..
} => dunder_next_error.return_type(db),
Self::IterCallError(dunder_iter_call_error) => dunder_iter_call_error
.fallback_return_type(db)
.try_call_dunder(db, "__next__", &CallArguments::none())
.map(|dunder_next_outcome| Some(dunder_next_outcome.return_type(db)))
.unwrap_or_else(|dunder_next_call_error| dunder_next_call_error.return_type(db)),
Self::PossiblyUnboundIterAndGetitemError {
dunder_next_return,
dunder_getitem_error,
} => match dunder_getitem_error {
CallDunderError::MethodNotAvailable => Some(*dunder_next_return),
CallDunderError::PossiblyUnbound(dunder_getitem_outcome) => {
Some(UnionType::from_elements(
db,
[*dunder_next_return, dunder_getitem_outcome.return_type(db)],
))
}
CallDunderError::Call(dunder_getitem_call_error) => Some(
dunder_getitem_call_error
.return_type(db)
.map(|dunder_getitem_return| {
let elements = [*dunder_next_return, dunder_getitem_return];
UnionType::from_elements(db, elements)
})
.unwrap_or(*dunder_next_return),
),
},
Self::UnboundIterAndGetitemError {
dunder_getitem_error,
} => dunder_getitem_error.return_type(db),
}
}
/// Reports the diagnostic for this error.
fn report_diagnostic(
&self,
context: &InferContext<'db>,
iterable_type: Type<'db>,
iterable_node: ast::AnyNodeRef,
) {
let db = context.db();
let report_not_iterable = |arguments: std::fmt::Arguments| {
context.report_lint(&NOT_ITERABLE, iterable_node, arguments);
};
// TODO: for all of these error variant, the "explanation" for the diagnostic
// (everything after the "because") should really be presented as a "help:", "note",
// or similar, rather than as part of the same sentence as the error message.
match self {
Self::IterCallError(dunder_iter_call_error) => match dunder_iter_call_error {
CallError::NotCallable { not_callable_type } => report_not_iterable(format_args!(
"Object of type `{iterable_type}` is not iterable \
because its `__iter__` attribute has type `{dunder_iter_type}`, \
which is not callable",
iterable_type = iterable_type.display(db),
dunder_iter_type = not_callable_type.display(db),
)),
CallError::PossiblyUnboundDunderCall { called_type, .. } => {
report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because its `__iter__` attribute (with type `{dunder_iter_type}`) \
may not be callable",
iterable_type = iterable_type.display(db),
dunder_iter_type = called_type.display(db),
));
}
CallError::Union(union_call_error) if union_call_error.indicates_type_possibly_not_callable() => {
report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because its `__iter__` attribute (with type `{dunder_iter_type}`) \
may not be callable",
iterable_type = iterable_type.display(db),
dunder_iter_type = union_call_error.called_type.display(db),
));
}
CallError::BindingError { .. } => report_not_iterable(format_args!(
"Object of type `{iterable_type}` is not iterable \
because its `__iter__` method has an invalid signature \
(expected `def __iter__(self): ...`)",
iterable_type = iterable_type.display(db),
)),
CallError::Union(UnionCallError { called_type, .. }) => report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because its `__iter__` method (with type `{dunder_iter_type}`) \
may have an invalid signature (expected `def __iter__(self): ...`)",
iterable_type = iterable_type.display(db),
dunder_iter_type = called_type.display(db),
)),
}
Self::IterReturnsInvalidIterator {
iterator,
dunder_next_error
} => match dunder_next_error {
CallDunderError::MethodNotAvailable => report_not_iterable(format_args!(
"Object of type `{iterable_type}` is not iterable \
because its `__iter__` method returns an object of type `{iterator_type}`, \
which has no `__next__` method",
iterable_type = iterable_type.display(db),
iterator_type = iterator.display(db),
)),
CallDunderError::PossiblyUnbound(_) => report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because its `__iter__` method returns an object of type `{iterator_type}`, \
which may not have a `__next__` method",
iterable_type = iterable_type.display(db),
iterator_type = iterator.display(db),
)),
CallDunderError::Call(dunder_next_call_error) => match dunder_next_call_error {
CallError::NotCallable { .. } => report_not_iterable(format_args!(
"Object of type `{iterable_type}` is not iterable \
because its `__iter__` method returns an object of type `{iterator_type}`, \
which has a `__next__` attribute that is not callable",
iterable_type = iterable_type.display(db),
iterator_type = iterator.display(db),
)),
CallError::PossiblyUnboundDunderCall { .. } => report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because its `__iter__` method returns an object of type `{iterator_type}`, \
which has a `__next__` attribute that may not be callable",
iterable_type = iterable_type.display(db),
iterator_type = iterator.display(db),
)),
CallError::Union(union_call_error) if union_call_error.indicates_type_possibly_not_callable() => {
report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because its `__iter__` method returns an object of type `{iterator_type}`, \
which has a `__next__` attribute that may not be callable",
iterable_type = iterable_type.display(db),
iterator_type = iterator.display(db),
));
}
CallError::BindingError { .. } => report_not_iterable(format_args!(
"Object of type `{iterable_type}` is not iterable \
because its `__iter__` method returns an object of type `{iterator_type}`, \
which has an invalid `__next__` method (expected `def __next__(self): ...`)",
iterable_type = iterable_type.display(db),
iterator_type = iterator.display(db),
)),
CallError::Union(_) => report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because its `__iter__` method returns an object of type `{iterator_type}`, \
which may have an invalid `__next__` method (expected `def __next__(self): ...`)",
iterable_type = iterable_type.display(db),
iterator_type = iterator.display(db),
)),
}
}
Self::PossiblyUnboundIterAndGetitemError {
dunder_getitem_error, ..
} => match dunder_getitem_error {
CallDunderError::MethodNotAvailable => report_not_iterable(format_args!(
"Object of type `{}` may not be iterable \
because it may not have an `__iter__` method \
and it doesn't have a `__getitem__` method",
iterable_type.display(db)
)),
CallDunderError::PossiblyUnbound(_) => report_not_iterable(format_args!(
"Object of type `{}` may not be iterable \
because it may not have an `__iter__` method or a `__getitem__` method",
iterable_type.display(db)
)),
CallDunderError::Call(dunder_getitem_call_error) => match dunder_getitem_call_error {
CallError::NotCallable { not_callable_type } => report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because it may not have an `__iter__` method \
and its `__getitem__` attribute has type `{dunder_getitem_type}`, \
which is not callable",
iterable_type = iterable_type.display(db),
dunder_getitem_type = not_callable_type.display(db),
)),
CallError::PossiblyUnboundDunderCall { .. } => report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because it may not have an `__iter__` method \
and its `__getitem__` attribute may not be callable",
iterable_type = iterable_type.display(db),
)),
CallError::Union(union_call_error) if union_call_error.indicates_type_possibly_not_callable() => {
report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because it may not have an `__iter__` method \
and its `__getitem__` attribute (with type `{dunder_getitem_type}`) \
may not be callable",
iterable_type = iterable_type.display(db),
dunder_getitem_type = union_call_error.called_type.display(db),
));
}
CallError::BindingError { .. } => report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because it may not have an `__iter__` method \
and its `__getitem__` method has an incorrect signature \
for the old-style iteration protocol \
(expected a signature at least as permissive as \
`def __getitem__(self, key: int): ...`)",
iterable_type = iterable_type.display(db),
)),
CallError::Union(UnionCallError {called_type, ..})=> report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because it may not have an `__iter__` method \
and its `__getitem__` method (with type `{dunder_getitem_type}`)
may have an incorrect signature for the old-style iteration protocol \
(expected a signature at least as permissive as \
`def __getitem__(self, key: int): ...`)",
iterable_type = iterable_type.display(db),
dunder_getitem_type = called_type.display(db),
)),
}
}
Self::UnboundIterAndGetitemError { dunder_getitem_error } => match dunder_getitem_error {
CallDunderError::MethodNotAvailable => report_not_iterable(format_args!(
"Object of type `{}` is not iterable because it doesn't have \
an `__iter__` method or a `__getitem__` method",
iterable_type.display(db)
)),
CallDunderError::PossiblyUnbound(_) => report_not_iterable(format_args!(
"Object of type `{}` may not be iterable because it has no `__iter__` method \
and it may not have a `__getitem__` method",
iterable_type.display(db)
)),
CallDunderError::Call(dunder_getitem_call_error) => match dunder_getitem_call_error {
CallError::NotCallable { not_callable_type } => report_not_iterable(format_args!(
"Object of type `{iterable_type}` is not iterable \
because it has no `__iter__` method and \
its `__getitem__` attribute has type `{dunder_getitem_type}`, \
which is not callable",
iterable_type = iterable_type.display(db),
dunder_getitem_type = not_callable_type.display(db),
)),
CallError::PossiblyUnboundDunderCall { .. } => report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because it has no `__iter__` method and its `__getitem__` attribute \
may not be callable",
iterable_type = iterable_type.display(db),
)),
CallError::Union(union_call_error) if union_call_error.indicates_type_possibly_not_callable() => {
report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because it has no `__iter__` method and its `__getitem__` attribute \
(with type `{dunder_getitem_type}`) may not be callable",
iterable_type = iterable_type.display(db),
dunder_getitem_type = union_call_error.called_type.display(db),
));
}
CallError::BindingError { .. } => report_not_iterable(format_args!(
"Object of type `{iterable_type}` is not iterable \
because it has no `__iter__` method and \
its `__getitem__` method has an incorrect signature \
for the old-style iteration protocol \
(expected a signature at least as permissive as \
`def __getitem__(self, key: int): ...`)",
iterable_type = iterable_type.display(db),
)),
CallError::Union(UnionCallError { called_type, .. }) => report_not_iterable(format_args!(
"Object of type `{iterable_type}` may not be iterable \
because it has no `__iter__` method and \
its `__getitem__` method (with type `{dunder_getitem_type}`) \
may have an incorrect signature for the old-style iteration protocol \
(expected a signature at least as permissive as \
`def __getitem__(self, key: int): ...`)",
iterable_type = iterable_type.display(db),
dunder_getitem_type = called_type.display(db),
)),
}
}
}
} }
} }

View file

@ -54,13 +54,13 @@ impl<'db> CallOutcome<'db> {
Ok(CallOutcome::Union(bindings.into())) Ok(CallOutcome::Union(bindings.into()))
} else if bindings.is_empty() && not_callable { } else if bindings.is_empty() && not_callable {
Err(CallError::NotCallable { Err(CallError::NotCallable {
not_callable_ty: Type::Union(union), not_callable_type: Type::Union(union),
}) })
} else { } else {
Err(CallError::Union(UnionCallError { Err(CallError::Union(UnionCallError {
errors: errors.into(), errors: errors.into(),
bindings: bindings.into(), bindings: bindings.into(),
called_ty: Type::Union(union), called_type: Type::Union(union),
})) }))
} }
} }
@ -89,7 +89,7 @@ pub(super) enum CallError<'db> {
/// The type is not callable. /// The type is not callable.
NotCallable { NotCallable {
/// The type that can't be called. /// The type that can't be called.
not_callable_ty: Type<'db>, not_callable_type: Type<'db>,
}, },
/// A call to a union failed because at least one variant /// A call to a union failed because at least one variant
@ -147,10 +147,10 @@ impl<'db> CallError<'db> {
pub(super) fn called_type(&self) -> Type<'db> { pub(super) fn called_type(&self) -> Type<'db> {
match self { match self {
Self::NotCallable { Self::NotCallable {
not_callable_ty, .. not_callable_type, ..
} => *not_callable_ty, } => *not_callable_type,
Self::Union(UnionCallError { called_ty, .. }) => *called_ty, Self::Union(UnionCallError { called_type, .. })
Self::PossiblyUnboundDunderCall { called_type, .. } => *called_type, | Self::PossiblyUnboundDunderCall { called_type, .. } => *called_type,
Self::BindingError { binding } => binding.callable_type(), Self::BindingError { binding } => binding.callable_type(),
} }
} }
@ -169,7 +169,29 @@ pub(super) struct UnionCallError<'db> {
pub(super) bindings: Box<[CallBinding<'db>]>, pub(super) bindings: Box<[CallBinding<'db>]>,
/// The union type that we tried calling. /// The union type that we tried calling.
pub(super) called_ty: Type<'db>, pub(super) called_type: Type<'db>,
}
impl UnionCallError<'_> {
/// Return `true` if this `UnionCallError` indicates that the union might not be callable at all.
/// Otherwise, return `false`.
///
/// For example, the union type `Callable[[int], int] | None` may not be callable at all,
/// because the `None` element in this union has no `__call__` method. Calling an object that
/// inhabited this union type would lead to a `UnionCallError` that would indicate that the
/// union might not be callable at all.
///
/// On the other hand, the union type `Callable[[int], int] | Callable[[str], str]` is always
/// *callable*, but it would still lead to a `UnionCallError` if an inhabitant of this type was
/// called with a single `int` argument passed in. That's because the second element in the
/// union doesn't accept an `int` when it's called: it only accepts a `str`.
pub(crate) fn indicates_type_possibly_not_callable(&self) -> bool {
self.errors.iter().any(|error| match error {
CallError::BindingError { .. } => false,
CallError::NotCallable { .. } | CallError::PossiblyUnboundDunderCall { .. } => true,
CallError::Union(union_error) => union_error.indicates_type_possibly_not_callable(),
})
}
} }
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@ -192,7 +214,7 @@ impl<'db> CallDunderError<'db> {
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> { pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self { match self {
Self::Call(error) => error.return_type(db), Self::Call(error) => error.return_type(db),
Self::PossiblyUnbound(_) => None, Self::PossiblyUnbound(call_outcome) => Some(call_outcome.return_type(db)),
Self::MethodNotAvailable => None, Self::MethodNotAvailable => None,
} }
} }

View file

@ -224,12 +224,12 @@ impl<'db> Class<'db> {
let return_ty_result = match metaclass.try_call(db, &arguments) { let return_ty_result = match metaclass.try_call(db, &arguments) {
Ok(outcome) => Ok(outcome.return_type(db)), Ok(outcome) => Ok(outcome.return_type(db)),
Err(CallError::NotCallable { not_callable_ty }) => Err(MetaclassError { Err(CallError::NotCallable { not_callable_type }) => Err(MetaclassError {
kind: MetaclassErrorKind::NotCallable(not_callable_ty), kind: MetaclassErrorKind::NotCallable(not_callable_type),
}), }),
Err(CallError::Union(UnionCallError { Err(CallError::Union(UnionCallError {
called_ty, called_type,
errors, errors,
bindings, bindings,
})) => { })) => {
@ -259,7 +259,7 @@ impl<'db> Class<'db> {
if partly_not_callable { if partly_not_callable {
Err(MetaclassError { Err(MetaclassError {
kind: MetaclassErrorKind::PartlyNotCallable(called_ty), kind: MetaclassErrorKind::PartlyNotCallable(called_type),
}) })
} else { } else {
Ok(return_ty.unwrap_or(Type::unknown())) Ok(return_ty.unwrap_or(Type::unknown()))

View file

@ -921,35 +921,6 @@ impl<'a> IntoIterator for &'a TypeCheckDiagnostics {
} }
} }
/// Emit a diagnostic declaring that the object represented by `node` is not iterable
pub(super) fn report_not_iterable(context: &InferContext, node: AnyNodeRef, not_iterable_ty: Type) {
context.report_lint(
&NOT_ITERABLE,
node,
format_args!(
"Object of type `{}` is not iterable",
not_iterable_ty.display(context.db())
),
);
}
/// Emit a diagnostic declaring that the object represented by `node` is not iterable
/// because its `__iter__` method is possibly unbound.
pub(super) fn report_not_iterable_possibly_unbound(
context: &InferContext,
node: AnyNodeRef,
element_ty: Type,
) {
context.report_lint(
&NOT_ITERABLE,
node,
format_args!(
"Object of type `{}` is not iterable because its `__iter__` method is possibly unbound",
element_ty.display(context.db())
),
);
}
/// Emit a diagnostic declaring that an index is out of bounds for a tuple. /// Emit a diagnostic declaring that an index is out of bounds for a tuple.
pub(super) fn report_index_out_of_bounds( pub(super) fn report_index_out_of_bounds(
context: &InferContext, context: &InferContext,

View file

@ -2414,7 +2414,7 @@ impl<'db> TypeInferenceBuilder<'db> {
} }
TargetKind::Name => iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| { TargetKind::Name => iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, iterable.into()); err.report_diagnostic(&self.context, iterable.into());
err.fallback_element_type() err.fallback_element_type(self.db())
}), }),
} }
}; };
@ -3209,7 +3209,7 @@ impl<'db> TypeInferenceBuilder<'db> {
} else { } else {
iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| { iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, iterable.into()); err.report_diagnostic(&self.context, iterable.into());
err.fallback_element_type() err.fallback_element_type(self.db())
}) })
}; };
@ -3436,13 +3436,13 @@ impl<'db> TypeInferenceBuilder<'db> {
call_expression: &ast::ExprCall, call_expression: &ast::ExprCall,
) { ) {
match err { match err {
CallError::NotCallable { not_callable_ty } => { CallError::NotCallable { not_callable_type } => {
context.report_lint( context.report_lint(
&CALL_NON_CALLABLE, &CALL_NON_CALLABLE,
call_expression, call_expression,
format_args!( format_args!(
"Object of type `{}` is not callable", "Object of type `{}` is not callable",
not_callable_ty.display(context.db()) not_callable_type.display(context.db())
), ),
); );
} }
@ -3492,7 +3492,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let iterable_ty = self.infer_expression(value); let iterable_ty = self.infer_expression(value);
iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| { iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, value.as_ref().into()); err.report_diagnostic(&self.context, value.as_ref().into());
err.fallback_element_type() err.fallback_element_type(self.db())
}); });
// TODO // TODO
@ -3511,7 +3511,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let iterable_ty = self.infer_expression(value); let iterable_ty = self.infer_expression(value);
iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| { iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, value.as_ref().into()); err.report_diagnostic(&self.context, value.as_ref().into());
err.fallback_element_type() err.fallback_element_type(self.db())
}); });
// TODO get type from `ReturnType` of generator // TODO get type from `ReturnType` of generator

View file

@ -59,7 +59,7 @@ impl<'db> Unpacker<'db> {
// type. // type.
value_ty = value_ty.try_iterate(self.db()).unwrap_or_else(|err| { value_ty = value_ty.try_iterate(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, value.as_any_node_ref(self.db())); err.report_diagnostic(&self.context, value.as_any_node_ref(self.db()));
err.fallback_element_type() err.fallback_element_type(self.db())
}); });
} }
@ -158,7 +158,7 @@ impl<'db> Unpacker<'db> {
} else { } else {
ty.try_iterate(self.db()).unwrap_or_else(|err| { ty.try_iterate(self.db()).unwrap_or_else(|err| {
err.report_diagnostic(&self.context, value_expr); err.report_diagnostic(&self.context, value_expr);
err.fallback_element_type() err.fallback_element_type(self.db())
}) })
}; };
for target_type in &mut target_types { for target_type in &mut target_types {