diff --git a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md b/crates/red_knot_python_semantic/resources/mdtest/loops/for.md index b2ef39956f..dfc753db60 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/loops/for.md +++ b/crates/red_knot_python_semantic/resources/mdtest/loops/for.md @@ -105,7 +105,11 @@ reveal_type(x) ## With non-callable iterator + + ```py +from typing_extensions import reveal_type + def _(flag: bool): class NotIterable: if flag: @@ -113,7 +117,8 @@ def _(flag: bool): else: __iter__: None = None - for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable" + # error: [not-iterable] + for x in NotIterable(): pass # revealed: Unknown @@ -123,21 +128,25 @@ def _(flag: bool): ## Invalid iterable + + ```py nonsense = 123 -for x in nonsense: # error: "Object of type `Literal[123]` is not iterable" +for x in nonsense: # error: [not-iterable] pass ``` ## New over old style iteration protocol + + ```py class NotIterable: def __getitem__(self, key: int) -> int: return 42 __iter__: None = None -for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable" +for x in NotIterable(): # error: [not-iterable] pass ``` @@ -221,7 +230,11 @@ def _(flag: bool): ## Union type as iterable where one union element has no `__iter__` method + + ```py +from typing_extensions import reveal_type + class TestIter: def __next__(self) -> int: return 42 @@ -231,14 +244,18 @@ class Test: return TestIter() 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: reveal_type(x) # revealed: int ``` ## Union type as iterable where one union element has invalid `__iter__` method + + ```py +from typing_extensions import reveal_type + class TestIter: def __next__(self) -> int: return 42 @@ -253,7 +270,7 @@ class Test2: def _(flag: bool): # 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(): reveal_type(x) # revealed: int ``` @@ -269,7 +286,454 @@ class Test: def __iter__(self) -> TestIter | int: 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(): 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 + + + +```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 + + + +```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 + + + +```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 + + + +```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__` + + + +```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 + + + +```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 + + + +```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 + + + +```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 + + + +```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 + + + +```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 + + + +```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__` + + + +```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 +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap new file mode 100644 index 0000000000..6a0db775ec --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Bad_`__getitem__`_method.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap new file mode 100644 index 0000000000..a8600a1547 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Invalid_iterable.snap @@ -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 + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap new file mode 100644 index 0000000000..59cf2147b8 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_New_over_old_style_iteration_protocol.snap @@ -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 + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap new file mode 100644 index 0000000000..321142e5c9 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_No_`__iter__`_method_and_`__getitem__`_is_not_callable.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap new file mode 100644 index 0000000000..a41e564a53 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly-not-callable_`__getitem__`_method.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap new file mode 100644 index 0000000000..9bda67201c --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__getitem__`_methods.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap new file mode 100644 index 0000000000..0b335416c6 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__iter__`_methods.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap new file mode 100644 index 0000000000..707f52e60b --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__next__`_method.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap new file mode 100644 index 0000000000..0a26d07d6f --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_bad_`__getitem__`_method.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap new file mode 100644 index 0000000000..61b533b7c3 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_invalid_`__getitem__`.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap new file mode 100644 index 0000000000..0bcd214cf4 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_unbound_`__iter__`_and_possibly_unbound_`__getitem__`.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap new file mode 100644 index 0000000000..22613e62d5 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_invalid_`__iter__`_method.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap new file mode 100644 index 0000000000..6eaa572ab6 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Union_type_as_iterable_where_one_union_element_has_no_`__iter__`_method.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap new file mode 100644 index 0000000000..70abd2ec9d --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_With_non-callable_iterator.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap new file mode 100644 index 0000000000..d5380a0d08 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_does_not_return_an_iterator.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap new file mode 100644 index 0000000000..2a1f5b67e4 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_method_with_a_bad_signature.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap new file mode 100644 index 0000000000..6a34e26bc8 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_`__iter__`_returns_an_iterator_with_an_invalid_`__next__`_method.snap @@ -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` + | + +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap index d4fd66b20c..6f2eb7f739 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap +++ b/crates/red_knot_python_semantic/resources/mdtest/snapshots/unpacking.md_-_Unpacking_-_Right_hand_side_not_iterable.snap @@ -22,7 +22,7 @@ error: lint:not-iterable --> /src/mdtest_snippet.py:1:8 | 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 | ``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 17002e939d..29df6db3fa 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use bitflags::bitflags; use call::{CallDunderError, CallError}; use context::InferContext; -use diagnostic::{report_not_iterable, report_not_iterable_possibly_unbound}; +use diagnostic::NOT_ITERABLE; use ruff_db::files::File; use ruff_python_ast as ast; use ruff_text_size::{Ranged, TextRange}; @@ -1737,7 +1737,7 @@ impl<'db> Type<'db> { // it still results in loosing information. Or should the information // be recomputed when rendering the diagnostic? 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 { union_error.errors.into_vec().pop().unwrap() } else { @@ -2237,15 +2237,15 @@ impl<'db> Type<'db> { // Turn "`` not callable" into // "`X` not callable" CallError::NotCallable { - not_callable_ty: self, + not_callable_type: self, } } CallDunderError::Call(CallError::Union(UnionCallError { - called_ty: _, + called_type: _, bindings, errors, })) => CallError::Union(UnionCallError { - called_ty: self, + called_type: self, bindings, errors, }), @@ -2261,7 +2261,7 @@ impl<'db> Type<'db> { CallDunderError::MethodNotAvailable => { // Turn "`X.__call__` unbound" into "`X` not callable" CallError::NotCallable { - not_callable_ty: self, + not_callable_type: self, } } }) @@ -2279,7 +2279,7 @@ impl<'db> Type<'db> { ))), _ => 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))), _ => 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. fn iterate(self, db: &'db dyn Db) -> Type<'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, @@ -2361,73 +2361,96 @@ impl<'db> Type<'db> { /// for y in x: /// pass /// ``` - fn try_iterate(self, db: &'db dyn Db) -> Result, IterateError<'db>> { + fn try_iterate(self, db: &'db dyn Db) -> Result, IterationError<'db>> { if let Type::Tuple(tuple_type) = self { return Ok(UnionType::from_elements(db, tuple_type.elements(db))); } - let dunder_iter_result = self.try_call_dunder(db, "__iter__", &CallArguments::none()); - match &dunder_iter_result { - Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => { - let iterator_ty = outcome.return_type(db); + let try_call_dunder_getitem = || { + self.try_call_dunder( + db, + "__getitem__", + &CallArguments::positional([KnownClass::Int.to_instance(db)]), + ) + .map(|dunder_getitem_outcome| dunder_getitem_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)) - } + let try_call_dunder_next_on_iterator = |iterator: Type<'db>| { + iterator + .try_call_dunder(db, "__next__", &CallArguments::none()) + .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, } - 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, - "__getitem__", - &CallArguments::positional([KnownClass::Int.to_instance(db)]), - ) { - Ok(outcome) => 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, - }), - } + // `__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] @@ -2909,51 +2932,348 @@ pub enum TypeVarBoundOrConstraints<'db> { Constraints(TupleType<'db>), } -/// Error returned if a type isn't iterable. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum IterateError<'db> { - /// The type isn't iterable because it doesn't implement the new-style or old-style iteration protocol - /// - /// 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> }, +/// Error returned if a type is not (or may not be) iterable. +#[derive(Debug)] +struct IterationError<'db> { + /// The type of the object that the analysed code attempted to iterate over. + iterable_type: Type<'db>, - /// The type is iterable but the methods aren't always bound. - PossiblyUnbound { - iterable_ty: Type<'db>, - element_ty: Type<'db>, - }, + /// The precise kind of error encountered when trying to iterate over the type. + error_kind: IterationErrorKind<'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. - fn element_type(&self) -> Option> { - match self { - IterateError::NotIterable { .. } => None, - IterateError::PossiblyUnbound { element_ty, .. } => Some(*element_ty), - } + fn element_type(&self, db: &'db dyn Db) -> Option> { + self.error_kind.element_type(db) } /// Returns the element type if it is known, or `Type::unknown()` if it is not. - fn fallback_element_type(&self) -> Type<'db> { - self.element_type().unwrap_or(Type::unknown()) + fn fallback_element_type(&self, db: &'db dyn Db) -> Type<'db> { + 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> { + 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), + )), + } + } + } } } diff --git a/crates/red_knot_python_semantic/src/types/call.rs b/crates/red_knot_python_semantic/src/types/call.rs index 8c45b7e154..e108a6656a 100644 --- a/crates/red_knot_python_semantic/src/types/call.rs +++ b/crates/red_knot_python_semantic/src/types/call.rs @@ -54,13 +54,13 @@ impl<'db> CallOutcome<'db> { Ok(CallOutcome::Union(bindings.into())) } else if bindings.is_empty() && not_callable { Err(CallError::NotCallable { - not_callable_ty: Type::Union(union), + not_callable_type: Type::Union(union), }) } else { Err(CallError::Union(UnionCallError { errors: errors.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. NotCallable { /// 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 @@ -147,10 +147,10 @@ impl<'db> CallError<'db> { pub(super) fn called_type(&self) -> Type<'db> { match self { Self::NotCallable { - not_callable_ty, .. - } => *not_callable_ty, - Self::Union(UnionCallError { called_ty, .. }) => *called_ty, - Self::PossiblyUnboundDunderCall { called_type, .. } => *called_type, + not_callable_type, .. + } => *not_callable_type, + Self::Union(UnionCallError { called_type, .. }) + | Self::PossiblyUnboundDunderCall { called_type, .. } => *called_type, Self::BindingError { binding } => binding.callable_type(), } } @@ -169,7 +169,29 @@ pub(super) struct UnionCallError<'db> { pub(super) bindings: Box<[CallBinding<'db>]>, /// 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)] @@ -192,7 +214,7 @@ impl<'db> CallDunderError<'db> { pub(super) fn return_type(&self, db: &'db dyn Db) -> Option> { match self { Self::Call(error) => error.return_type(db), - Self::PossiblyUnbound(_) => None, + Self::PossiblyUnbound(call_outcome) => Some(call_outcome.return_type(db)), Self::MethodNotAvailable => None, } } diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 22a9a6fa08..08878e6443 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -224,12 +224,12 @@ impl<'db> Class<'db> { let return_ty_result = match metaclass.try_call(db, &arguments) { Ok(outcome) => Ok(outcome.return_type(db)), - Err(CallError::NotCallable { not_callable_ty }) => Err(MetaclassError { - kind: MetaclassErrorKind::NotCallable(not_callable_ty), + Err(CallError::NotCallable { not_callable_type }) => Err(MetaclassError { + kind: MetaclassErrorKind::NotCallable(not_callable_type), }), Err(CallError::Union(UnionCallError { - called_ty, + called_type, errors, bindings, })) => { @@ -259,7 +259,7 @@ impl<'db> Class<'db> { if partly_not_callable { Err(MetaclassError { - kind: MetaclassErrorKind::PartlyNotCallable(called_ty), + kind: MetaclassErrorKind::PartlyNotCallable(called_type), }) } else { Ok(return_ty.unwrap_or(Type::unknown())) diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index 29538ac353..7221f8bb11 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -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. pub(super) fn report_index_out_of_bounds( context: &InferContext, diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 5bc7fb350c..c847f146c4 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2414,7 +2414,7 @@ impl<'db> TypeInferenceBuilder<'db> { } TargetKind::Name => iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| { 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 { iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| { 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, ) { match err { - CallError::NotCallable { not_callable_ty } => { + CallError::NotCallable { not_callable_type } => { context.report_lint( &CALL_NON_CALLABLE, call_expression, format_args!( "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); iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| { err.report_diagnostic(&self.context, value.as_ref().into()); - err.fallback_element_type() + err.fallback_element_type(self.db()) }); // TODO @@ -3511,7 +3511,7 @@ impl<'db> TypeInferenceBuilder<'db> { let iterable_ty = self.infer_expression(value); iterable_ty.try_iterate(self.db()).unwrap_or_else(|err| { 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 diff --git a/crates/red_knot_python_semantic/src/types/unpacker.rs b/crates/red_knot_python_semantic/src/types/unpacker.rs index 40a20e4e86..21c17dad13 100644 --- a/crates/red_knot_python_semantic/src/types/unpacker.rs +++ b/crates/red_knot_python_semantic/src/types/unpacker.rs @@ -59,7 +59,7 @@ impl<'db> Unpacker<'db> { // type. 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.fallback_element_type() + err.fallback_element_type(self.db()) }); } @@ -158,7 +158,7 @@ impl<'db> Unpacker<'db> { } else { ty.try_iterate(self.db()).unwrap_or_else(|err| { err.report_diagnostic(&self.context, value_expr); - err.fallback_element_type() + err.fallback_element_type(self.db()) }) }; for target_type in &mut target_types {