diff --git a/crates/ty/docs/rules.md b/crates/ty/docs/rules.md
index 7769d823bf183..402efcea59e8b 100644
--- a/crates/ty/docs/rules.md
+++ b/crates/ty/docs/rules.md
@@ -8,7 +8,7 @@
Default level: error ·
Added in 0.0.13 ·
Related issues ·
-View source
+View source
@@ -49,7 +49,7 @@ class Derived(Base): # Error: `Derived` does not implement `method`
Default level: warn ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -90,7 +90,7 @@ class SubProto(BaseProto, Protocol):
Default level: error ·
Added in 0.0.14 ·
Related issues ·
-View source
+View source
@@ -157,7 +157,7 @@ def test(): -> "int":
Default level: error ·
Preview (since 0.0.16) ·
Related issues ·
-View source
+View source
@@ -206,7 +206,7 @@ Foo.method() # Error: cannot call abstract classmethod
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -230,7 +230,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
Default level: error ·
Added in 0.0.7 ·
Related issues ·
-View source
+View source
@@ -261,7 +261,7 @@ def f(x: object):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -293,7 +293,7 @@ f(int) # error
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -324,7 +324,7 @@ a = 1
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -356,7 +356,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -388,7 +388,7 @@ class B(A): ...
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -416,7 +416,7 @@ type B = A
Default level: error ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -448,7 +448,7 @@ class Example:
Default level: warn ·
Added in 0.0.1-alpha.16 ·
Related issues ·
-View source
+View source
@@ -475,7 +475,7 @@ old_func() # emits [deprecated] diagnostic
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -504,7 +504,7 @@ false positives it can produce.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -531,7 +531,7 @@ class B(A, A): ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -569,7 +569,7 @@ class A: # Crash at runtime
Default level: error ·
Added in 0.0.14 ·
Related issues ·
-View source
+View source
@@ -640,7 +640,7 @@ def foo() -> "intt\b": ...
Default level: error ·
Added in 0.0.20 ·
Related issues ·
-View source
+View source
@@ -672,7 +672,7 @@ def my_function() -> int:
Default level: error ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -798,7 +798,7 @@ def test(): -> "Literal[5]":
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -828,7 +828,7 @@ class C(A, B): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -854,7 +854,7 @@ t[3] # IndexError: tuple index out of range
Default level: warn ·
Added in 0.0.1-alpha.33 ·
Related issues ·
-View source
+View source
@@ -888,7 +888,7 @@ class MyClass: ...
Default level: error ·
Added in 0.0.1-alpha.12 ·
Related issues ·
-View source
+View source
@@ -977,7 +977,7 @@ an atypical memory layout.
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1004,7 +1004,7 @@ func("foo") # error: [invalid-argument-type]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1032,7 +1032,7 @@ a: int = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1066,7 +1066,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -1102,7 +1102,7 @@ asyncio.run(main())
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1126,7 +1126,7 @@ class A(42): ... # error: [invalid-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1153,7 +1153,7 @@ with 1:
Default level: error ·
Added in 0.0.12 ·
Related issues ·
-View source
+View source
@@ -1190,7 +1190,7 @@ class Foo(NamedTuple):
Default level: error ·
Added in 0.0.13 ·
Related issues ·
-View source
+View source
@@ -1222,7 +1222,7 @@ class A:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1251,7 +1251,7 @@ a: str
Default level: warn ·
Added in 0.0.20 ·
Related issues ·
-View source
+View source
@@ -1300,7 +1300,7 @@ class Pet(Enum):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1344,7 +1344,7 @@ except ZeroDivisionError:
Default level: error ·
Added in 0.0.1-alpha.28 ·
Related issues ·
-View source
+View source
@@ -1386,7 +1386,7 @@ class D(A):
Default level: error ·
Added in 0.0.1-alpha.35 ·
Related issues ·
-View source
+View source
@@ -1430,7 +1430,7 @@ class NonFrozenChild(FrozenBase): # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1468,7 +1468,7 @@ class D(Generic[U, T]): ...
Default level: error ·
Added in 0.0.12 ·
Related issues ·
-View source
+View source
@@ -1547,7 +1547,7 @@ a = 20 / 0 # type: ignore
Default level: error ·
Added in 0.0.1-alpha.17 ·
Related issues ·
-View source
+View source
@@ -1586,7 +1586,7 @@ carol = Person(name="Carol", aeg=25) # typo!
Default level: warn ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -1647,7 +1647,7 @@ def f(x, y, /): # Python 3.8+ syntax
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1682,7 +1682,7 @@ def f(t: TypeVar("U")): ...
Default level: error ·
Added in 0.0.18 ·
Related issues ·
-View source
+View source
@@ -1710,7 +1710,7 @@ match x:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1744,7 +1744,7 @@ class B(metaclass=f): ...
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -1851,7 +1851,7 @@ Correct use of `@override` is enforced by ty's [`invalid-explicit-override`](#in
Default level: error ·
Added in 0.0.1-alpha.19 ·
Related issues ·
-View source
+View source
@@ -1905,7 +1905,7 @@ AttributeError: Cannot overwrite NamedTuple attribute _asdict
Default level: error ·
Added in 0.0.1-alpha.27 ·
Related issues ·
-View source
+View source
@@ -1935,7 +1935,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -1985,7 +1985,7 @@ def foo(x: int) -> int: ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2011,7 +2011,7 @@ def f(a: int = ''): ...
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2042,7 +2042,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2076,7 +2076,7 @@ TypeError: Protocols can only inherit from other protocols, got
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2125,7 +2125,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2154,7 +2154,7 @@ def func() -> int:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2250,7 +2250,7 @@ class C: ...
Default level: error ·
Added in 0.0.10 ·
Related issues ·
-View source
+View source
@@ -2296,7 +2296,7 @@ class MyClass:
Default level: error ·
Added in 0.0.1-alpha.6 ·
Related issues ·
-View source
+View source
@@ -2323,7 +2323,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -2370,7 +2370,7 @@ Bar[int] # error: too few arguments
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2400,7 +2400,7 @@ TYPE_CHECKING = ''
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2430,7 +2430,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -2464,7 +2464,7 @@ f(10) # Error
Default level: error ·
Added in 0.0.1-alpha.11 ·
Related issues ·
-View source
+View source
@@ -2498,7 +2498,7 @@ class C:
Default level: error ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -2529,7 +2529,7 @@ def g[U, T: U](): ... # error: [invalid-type-variable-bound]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2576,7 +2576,7 @@ U = TypeVar('U', list[int], int) # valid constrained Type
Default level: error ·
Added in 0.0.16 ·
Related issues ·
-View source
+View source
@@ -2608,7 +2608,7 @@ U = TypeVar("U", int, str, default=bytes) # error: [invalid-type-variable-defau
Default level: error ·
Added in 0.0.14 ·
Related issues ·
-View source
+View source
@@ -2643,7 +2643,7 @@ def f(x: dict):
Default level: error ·
Added in 0.0.9 ·
Related issues ·
-View source
+View source
@@ -2668,13 +2668,44 @@ class Foo(TypedDict):
pass
```
+## `invalid-yield`
+
+
+Default level: error ·
+Added in 0.0.25 ·
+Related issues ·
+View source
+
+
+
+**What it does**
+
+Detects `yield` and `yield from` expressions where the "yield" or "send" type
+is incompatible with the generator function's annotated return type.
+
+**Why is this bad?**
+
+Yielding a value of a type that doesn't match the generator's declared yield type,
+or using `yield from` with a sub-iterator whose yield or send type is incompatible,
+is a type error that may cause downstream consumers of the generator to receive
+values of an unexpected type.
+
+**Examples**
+
+```python
+from typing import Iterator
+
+def gen() -> Iterator[int]:
+ yield "not an int" # error: [invalid-yield]
+```
+
## `isinstance-against-protocol`
Default level: error ·
Added in 0.0.14 ·
Related issues ·
-View source
+View source
@@ -2729,7 +2760,7 @@ def h(arg2: type):
Default level: error ·
Added in 0.0.15 ·
Related issues ·
-View source
+View source
@@ -2772,7 +2803,7 @@ def g(arg: object):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2797,7 +2828,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
Default level: error ·
Added in 0.0.1-alpha.20 ·
Related issues ·
-View source
+View source
@@ -2830,7 +2861,7 @@ alice["age"] # KeyError
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2859,7 +2890,7 @@ func("string") # error: [no-matching-overload]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2885,7 +2916,7 @@ for i in 34: # TypeError: 'int' object is not iterable
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -2909,7 +2940,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
Default level: error ·
Added in 0.0.1-alpha.29 ·
Related issues ·
-View source
+View source
@@ -2942,7 +2973,7 @@ class B(A):
Default level: error ·
Added in 0.0.16 ·
Related issues ·
-View source
+View source
@@ -2975,7 +3006,7 @@ class B(A):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3002,7 +3033,7 @@ f(1, x=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -3029,7 +3060,7 @@ f(x=1) # Error raised here
Default level: ignore ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -3062,7 +3093,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -3094,7 +3125,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
Default level: ignore ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -3131,7 +3162,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
Default level: warn ·
Added in 0.0.23 ·
Related issues ·
-View source
+View source
@@ -3158,7 +3189,7 @@ html.parser # AttributeError: module 'html' has no attribute 'parser'
Default level: ignore ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3222,7 +3253,7 @@ def test(): -> "int":
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3249,7 +3280,7 @@ cast(int, f()) # Redundant
Default level: warn ·
Added in 0.0.18 ·
Related issues ·
-View source
+View source
@@ -3281,7 +3312,7 @@ class C:
Default level: error ·
Added in 0.0.20 ·
Related issues ·
-View source
+View source
@@ -3315,7 +3346,7 @@ class Outer[T]:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3345,7 +3376,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3374,7 +3405,7 @@ class B(A): ... # Error raised here
Default level: error ·
Added in 0.0.1-alpha.30 ·
Related issues ·
-View source
+View source
@@ -3408,7 +3439,7 @@ class F(NamedTuple):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3435,7 +3466,7 @@ f("foo") # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3463,7 +3494,7 @@ def _(x: int):
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3509,7 +3540,7 @@ class A:
Default level: error ·
Added in 0.0.20 ·
Related issues ·
-View source
+View source
@@ -3546,7 +3577,7 @@ class C(Generic[T]):
Default level: warn ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3570,7 +3601,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3597,7 +3628,7 @@ f(x=1, y=2) # Error raised here
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3625,7 +3656,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
Default level: warn ·
Added in 0.0.1-alpha.15 ·
Related issues ·
-View source
+View source
@@ -3683,7 +3714,7 @@ def g():
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3708,7 +3739,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3733,7 +3764,7 @@ print(x) # NameError: name 'x' is not defined
Default level: warn ·
Added in 0.0.1-alpha.7 ·
Related issues ·
-View source
+View source
@@ -3772,7 +3803,7 @@ class D(C): ... # error: [unsupported-base]
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3809,7 +3840,7 @@ b1 < b2 < b1 # exception raised here
Default level: ignore ·
Added in 0.0.12 ·
Related issues ·
-View source
+View source
@@ -3849,7 +3880,7 @@ def factory(base: type[Base]) -> type:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
@@ -3877,7 +3908,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
Default level: warn ·
Preview (since 0.0.21) ·
Related issues ·
-View source
+View source
@@ -3983,7 +4014,7 @@ to `false`.
Default level: warn ·
Added in 0.0.1-alpha.22 ·
Related issues ·
-View source
+View source
@@ -4046,7 +4077,7 @@ def foo(x: int | str) -> int | str:
Default level: error ·
Added in 0.0.1-alpha.1 ·
Related issues ·
-View source
+View source
diff --git a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md
index a207b3414fa58..7dd74a3c96298 100644
--- a/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md
+++ b/crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md
@@ -13,8 +13,7 @@ def inner_generator() -> Generator[int, bytes, str]:
yield 2
x = yield 3
- # TODO: this should be `bytes`
- reveal_type(x) # revealed: @Todo(yield expressions)
+ reveal_type(x) # revealed: bytes
return "done"
@@ -82,8 +81,7 @@ def inner_generator() -> GeneratorType[int, bytes, str]:
yield 2
x = yield 3
- # TODO: this should be `bytes`
- reveal_type(x) # revealed: @Todo(yield expressions)
+ reveal_type(x) # revealed: bytes
return "done"
@@ -92,6 +90,88 @@ def outer_generator():
reveal_type(result) # revealed: str
```
+## Infering with type context
+
+A dict literal that is structurally compatible with a `TypedDict` should be accepted.
+
+```py
+from typing import Iterator, TypedDict
+
+class Person(TypedDict):
+ name: str
+
+def persons() -> Iterator[Person]:
+ yield {"name": "Alice"}
+ yield {"name": "Bob"}
+
+ # error: [invalid-yield]
+ # error: [invalid-argument-type]
+ yield {"name": 42}
+```
+
+This also works with `yield from`, where the iterable expression is inferred with the outer
+generator's yield type as type context:
+
+```py
+def persons() -> Iterator[Person]:
+ yield from [{"name": "Alice"}, {"name": "Bob"}]
+
+ # error: [invalid-yield]
+ # error: [invalid-argument-type]
+ yield from [{"name": 42}]
+```
+
+## `yield` expression send type inference
+
+```py
+from typing import AsyncGenerator, AsyncIterator, Generator, Iterator
+
+def unannotated():
+ x = yield 1
+ reveal_type(x) # revealed: Unknown
+
+def default_generator() -> Generator:
+ x = yield
+ reveal_type(x) # revealed: None
+
+def generator_one_arg() -> Generator[int]:
+ x = yield 1
+ reveal_type(x) # revealed: None
+
+def generator_send_str() -> Generator[int, str]:
+ x = yield 1
+ reveal_type(x) # revealed: str
+
+async def async_generator_default() -> AsyncGenerator[int]:
+ x = yield 1
+ reveal_type(x) # revealed: None
+
+async def async_generator_send_str() -> AsyncGenerator[int, str]:
+ x = yield 1
+ reveal_type(x) # revealed: str
+
+def mixing_generator_async_generator() -> Generator[int, int, None] | AsyncGenerator[int, str]:
+ x = yield 1
+ reveal_type(x) # revealed: int | str
+ return None
+```
+
+`Iterator` has no send type or return type, It is equivalent to using `Generator` with send set to
+`None` and return type to `Unknown`.
+
+```py
+def iterator_send_none() -> Iterator[int]:
+ x = yield 1
+ reveal_type(x) # revealed: None
+
+async def async_iterator_send_none() -> AsyncIterator[int]:
+ x = yield 1
+ reveal_type(x) # revealed: None
+
+def iterator_yield_from() -> Generator[int, None, int]:
+ yield from iterator_send_none()
+```
+
## Error cases
### Non-iterable type
@@ -105,12 +185,28 @@ def generator() -> Generator:
### Invalid `yield` type
+
+
```py
from typing import Generator
-# TODO: This should be an error. Claims to yield `int`, but yields `str`.
def invalid_generator() -> Generator[int, None, None]:
- yield "not an int" # This should be an `int`
+ # error: [invalid-yield] "Yield type `Literal[""]` does not match annotated yield type `int`"
+ yield ""
+```
+
+### Invalid annotation
+
+```py
+from typing import AsyncGenerator, Generator
+
+def returns_str() -> str: # error: [invalid-return-type]
+ x = yield 1
+ reveal_type(x) # revealed: Unknown
+
+def sync_returns_async_generator() -> AsyncGenerator[int, str]: # error: [invalid-return-type]
+ x = yield 1
+ reveal_type(x) # revealed: str
```
### Invalid return type
@@ -128,3 +224,31 @@ def invalid_generator2() -> Generator[int, None, None]:
return "done"
```
+
+### `yield from` with incompatible yield type
+
+```py
+from typing import Generator
+
+def inner() -> Generator[str, None, None]:
+ yield "hello"
+
+def outer() -> Generator[int, None, None]:
+ # error: [invalid-yield] "Yield type `str` does not match annotated yield type `int`"
+ yield from inner()
+```
+
+### `yield from` with incompatible send type
+
+
+
+```py
+from typing import Generator
+
+def inner() -> Generator[int, int, None]:
+ x = yield 1
+
+def outer() -> Generator[int, str, None]:
+ # error: [invalid-yield] "Send type `int` does not match annotated send type `str`"
+ yield from inner()
+```
diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/yield_and_yield_from\342\200\246_-_`yield`_and_`yield_f\342\200\246_-_Error_cases_-_Invalid_`yield`_type_(1300c06a97026cce).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/yield_and_yield_from\342\200\246_-_`yield`_and_`yield_f\342\200\246_-_Error_cases_-_Invalid_`yield`_type_(1300c06a97026cce).snap"
new file mode 100644
index 0000000000000..2947a42b3eb61
--- /dev/null
+++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/yield_and_yield_from\342\200\246_-_`yield`_and_`yield_f\342\200\246_-_Error_cases_-_Invalid_`yield`_type_(1300c06a97026cce).snap"
@@ -0,0 +1,39 @@
+---
+source: crates/ty_test/src/lib.rs
+expression: snapshot
+---
+
+---
+mdtest name: yield_and_yield_from.md - `yield` and `yield from` - Error cases - Invalid `yield` type
+mdtest path: crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md
+---
+
+# Python source files
+
+## mdtest_snippet.py
+
+```
+1 | from typing import Generator
+2 |
+3 | def invalid_generator() -> Generator[int, None, None]:
+4 | # error: [invalid-yield] "Yield type `Literal[""]` does not match annotated yield type `int`"
+5 | yield ""
+```
+
+# Diagnostics
+
+```
+error[invalid-yield]: Yield expression type does not match annotation
+ --> src/mdtest_snippet.py:3:28
+ |
+1 | from typing import Generator
+2 |
+3 | def invalid_generator() -> Generator[int, None, None]:
+ | -------------------------- Function annotated with yield type `int` here
+4 | # error: [invalid-yield] "Yield type `Literal[""]` does not match annotated yield type `int`"
+5 | yield ""
+ | ^^ expression of type `Literal[""]`, expected `int`
+ |
+info: rule `invalid-yield` is enabled by default
+
+```
diff --git "a/crates/ty_python_semantic/resources/mdtest/snapshots/yield_and_yield_from\342\200\246_-_`yield`_and_`yield_f\342\200\246_-_Error_cases_-_`yield_from`_with_in\342\200\246_(63388cb3d15fdc10).snap" "b/crates/ty_python_semantic/resources/mdtest/snapshots/yield_and_yield_from\342\200\246_-_`yield`_and_`yield_f\342\200\246_-_Error_cases_-_`yield_from`_with_in\342\200\246_(63388cb3d15fdc10).snap"
new file mode 100644
index 0000000000000..278608fe9a12d
--- /dev/null
+++ "b/crates/ty_python_semantic/resources/mdtest/snapshots/yield_and_yield_from\342\200\246_-_`yield`_and_`yield_f\342\200\246_-_Error_cases_-_`yield_from`_with_in\342\200\246_(63388cb3d15fdc10).snap"
@@ -0,0 +1,42 @@
+---
+source: crates/ty_test/src/lib.rs
+expression: snapshot
+---
+
+---
+mdtest name: yield_and_yield_from.md - `yield` and `yield from` - Error cases - `yield from` with incompatible send type
+mdtest path: crates/ty_python_semantic/resources/mdtest/expression/yield_and_yield_from.md
+---
+
+# Python source files
+
+## mdtest_snippet.py
+
+```
+1 | from typing import Generator
+2 |
+3 | def inner() -> Generator[int, int, None]:
+4 | x = yield 1
+5 |
+6 | def outer() -> Generator[int, str, None]:
+7 | # error: [invalid-yield] "Send type `int` does not match annotated send type `str`"
+8 | yield from inner()
+```
+
+# Diagnostics
+
+```
+error[invalid-yield]: Send type does not match annotation
+ --> src/mdtest_snippet.py:6:16
+ |
+4 | x = yield 1
+5 |
+6 | def outer() -> Generator[int, str, None]:
+ | ------------------------- Function annotated with send type `str` here
+7 | # error: [invalid-yield] "Send type `int` does not match annotated send type `str`"
+8 | yield from inner()
+ | ^^^^^^^ generator with send type `int`, expected `str`
+ |
+info: rule `invalid-yield` is enabled by default
+
+```
diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs
index c2f38727e1e9a..ccbeac434de70 100644
--- a/crates/ty_python_semantic/src/types.rs
+++ b/crates/ty_python_semantic/src/types.rs
@@ -758,6 +758,14 @@ fn recursive_type_normalize_type_guard_like<'db, T: TypeGuardLike<'db>>(
Some(guard.with_type(db, ty))
}
+#[derive(Debug, Clone, Copy)]
+#[expect(clippy::struct_field_names)]
+struct GeneratorTypes<'db> {
+ yield_ty: Option>,
+ send_ty: Option>,
+ return_ty: Option>,
+}
+
#[salsa::tracked]
impl<'db> Type<'db> {
pub(crate) const fn any() -> Self {
@@ -4752,7 +4760,7 @@ impl<'db> Type<'db> {
///
/// This corresponds to the `ReturnT` parameter of the generic `typing.Generator[YieldT, SendT, ReturnT]`
/// protocol.
- fn generator_return_type(self, db: &'db dyn Db) -> Option> {
+ fn generator_types(self, db: &'db dyn Db) -> Option> {
// TODO: Ideally, we would first try to upcast `self` to an instance of `Generator` and *then*
// match on the protocol instance to get the `ReturnType` type parameter. For now, implement
// an ad-hoc solution that works for protocols and instances of classes that explicitly inherit
@@ -4760,12 +4768,36 @@ impl<'db> Type<'db> {
let from_class_base = |base: ClassBase<'db>| {
let class = base.into_class()?;
+ let (_, Some(specialization)) = class.static_class_literal_specialized(db, None)?
+ else {
+ return None;
+ };
+
if class.is_known(db, KnownClass::Generator)
- && let Some((_, Some(specialization))) =
- class.static_class_literal_specialized(db, None)
- && let [_, _, return_ty] = specialization.types(db)
+ && let [yield_ty, send_ty, return_ty] = specialization.types(db)
+ {
+ Some(GeneratorTypes {
+ yield_ty: Some(*yield_ty),
+ send_ty: Some(*send_ty),
+ return_ty: Some(*return_ty),
+ })
+ } else if class.is_known(db, KnownClass::AsyncGenerator)
+ && let [yield_ty, send_ty] = specialization.types(db)
+ {
+ Some(GeneratorTypes {
+ yield_ty: Some(*yield_ty),
+ send_ty: Some(*send_ty),
+ return_ty: None,
+ })
+ } else if (class.is_known(db, KnownClass::Iterator)
+ || class.is_known(db, KnownClass::AsyncIterator))
+ && let [yield_ty] = specialization.types(db)
{
- Some(*return_ty)
+ Some(GeneratorTypes {
+ yield_ty: Some(*yield_ty),
+ send_ty: Some(Type::none(db)),
+ return_ty: Some(Type::unknown()),
+ })
} else {
None
}
@@ -4782,26 +4814,96 @@ impl<'db> Type<'db> {
None
}
}
- Type::Union(union) => union.try_map(db, |ty| ty.generator_return_type(db)),
+ Type::Union(union) => {
+ let mut yield_builder = Some(UnionBuilder::new(db));
+ let mut send_builder = Some(UnionBuilder::new(db));
+ let mut return_builder = Some(UnionBuilder::new(db));
+
+ for ty in union.elements(db) {
+ let gt = ty.generator_types(db)?;
+ match gt.yield_ty {
+ Some(ty) => yield_builder = yield_builder.map(|b| b.add(ty)),
+ None => yield_builder = None,
+ }
+ match gt.send_ty {
+ Some(ty) => send_builder = send_builder.map(|b| b.add(ty)),
+ None => send_builder = None,
+ }
+ match gt.return_ty {
+ Some(ty) => return_builder = return_builder.map(|b| b.add(ty)),
+ None => return_builder = None,
+ }
+ }
+
+ Some(GeneratorTypes {
+ yield_ty: yield_builder.map(UnionBuilder::build),
+ send_ty: send_builder.map(UnionBuilder::build),
+ return_ty: return_builder.map(UnionBuilder::build),
+ })
+ }
Type::Intersection(intersection) => {
- let mut builder = IntersectionBuilder::new(db);
- let mut any_success = false;
// Using `positive()` rather than `positive_elements_or_object()` is safe
// here because `object` is not a generator, so falling back to it would
// still return `None`.
+ let mut yield_builder = Some(IntersectionBuilder::new(db));
+ let mut send_builder = Some(IntersectionBuilder::new(db));
+ let mut return_builder = Some(IntersectionBuilder::new(db));
+ let mut any_success = false;
+
for ty in intersection.positive(db) {
- if let Some(return_ty) = ty.generator_return_type(db) {
- builder = builder.add_positive(return_ty);
- any_success = true;
+ let Some(gt) = ty.generator_types(db) else {
+ continue;
+ };
+ any_success = true;
+ match gt.yield_ty {
+ Some(ty) => {
+ yield_builder = yield_builder.map(|b| b.add_positive(ty));
+ }
+ None => yield_builder = None,
+ }
+ match gt.send_ty {
+ Some(ty) => {
+ send_builder = send_builder.map(|b| b.add_positive(ty));
+ }
+ None => send_builder = None,
+ }
+ match gt.return_ty {
+ Some(ty) => {
+ return_builder = return_builder.map(|b| b.add_positive(ty));
+ }
+ None => return_builder = None,
}
}
- any_success.then(|| builder.build())
+
+ if !any_success {
+ return None;
+ }
+
+ Some(GeneratorTypes {
+ yield_ty: yield_builder.map(IntersectionBuilder::build),
+ send_ty: send_builder.map(IntersectionBuilder::build),
+ return_ty: return_builder.map(IntersectionBuilder::build),
+ })
}
- ty @ (Type::Dynamic(_) | Type::Never) => Some(ty),
+ ty @ (Type::Dynamic(_) | Type::Never) => Some(GeneratorTypes {
+ yield_ty: Some(ty),
+ send_ty: Some(ty),
+ return_ty: Some(ty),
+ }),
_ => None,
}
}
+ fn generator_return_type(self, db: &'db dyn Db) -> Option> {
+ self.generator_types(db)
+ .and_then(|generator_types| generator_types.return_ty)
+ }
+
+ fn generator_send_type(self, db: &'db dyn Db) -> Option> {
+ self.generator_types(db)
+ .and_then(|generator_types| generator_types.send_ty)
+ }
+
#[must_use]
pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option> {
match self {
diff --git a/crates/ty_python_semantic/src/types/class/known.rs b/crates/ty_python_semantic/src/types/class/known.rs
index 37191ddc509bf..e071628535b64 100644
--- a/crates/ty_python_semantic/src/types/class/known.rs
+++ b/crates/ty_python_semantic/src/types/class/known.rs
@@ -91,6 +91,7 @@ pub enum KnownClass {
// Typing
Awaitable,
Generator,
+ AsyncGenerator,
Deprecated,
StdlibAlias,
SpecialForm,
@@ -108,6 +109,7 @@ pub enum KnownClass {
SupportsIndex,
Iterable,
Iterator,
+ AsyncIterator,
Sequence,
Mapping,
// typing_extensions
@@ -226,6 +228,7 @@ impl KnownClass {
| Self::ABCMeta
| Self::Iterable
| Self::Iterator
+ | Self::AsyncIterator
| Self::Sequence
| Self::Mapping
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
@@ -236,6 +239,7 @@ impl KnownClass {
| Self::Classmethod
| Self::Awaitable
| Self::Generator
+ | Self::AsyncGenerator
| Self::Deprecated
| Self::Field
| Self::KwOnly
@@ -281,6 +285,7 @@ impl KnownClass {
| KnownClass::Classmethod
| KnownClass::Awaitable
| KnownClass::Generator
+ | KnownClass::AsyncGenerator
| KnownClass::Deprecated
| KnownClass::Super
| KnownClass::Enum
@@ -316,6 +321,7 @@ impl KnownClass {
| KnownClass::SupportsIndex
| KnownClass::Iterable
| KnownClass::Iterator
+ | KnownClass::AsyncIterator
| KnownClass::Sequence
| KnownClass::Mapping
| KnownClass::ChainMap
@@ -370,6 +376,7 @@ impl KnownClass {
| KnownClass::Classmethod
| KnownClass::Awaitable
| KnownClass::Generator
+ | KnownClass::AsyncGenerator
| KnownClass::Deprecated
| KnownClass::Super
| KnownClass::Enum
@@ -405,6 +412,7 @@ impl KnownClass {
| KnownClass::SupportsIndex
| KnownClass::Iterable
| KnownClass::Iterator
+ | KnownClass::AsyncIterator
| KnownClass::Sequence
| KnownClass::Mapping
| KnownClass::ChainMap
@@ -459,6 +467,7 @@ impl KnownClass {
| KnownClass::Classmethod
| KnownClass::Awaitable
| KnownClass::Generator
+ | KnownClass::AsyncGenerator
| KnownClass::Deprecated
| KnownClass::Super
| KnownClass::Enum
@@ -494,6 +503,7 @@ impl KnownClass {
| KnownClass::SupportsIndex
| KnownClass::Iterable
| KnownClass::Iterator
+ | KnownClass::AsyncIterator
| KnownClass::Sequence
| KnownClass::Mapping
| KnownClass::ChainMap
@@ -536,8 +546,10 @@ impl KnownClass {
Self::SupportsIndex
| Self::Iterable
| Self::Iterator
+ | Self::AsyncIterator
| Self::Awaitable
| Self::NamedTupleLike
+ | Self::AsyncGenerator
| Self::Generator => true,
Self::Bool
@@ -674,6 +686,7 @@ impl KnownClass {
| KnownClass::NoneType
| KnownClass::Awaitable
| KnownClass::Generator
+ | KnownClass::AsyncGenerator
| KnownClass::Deprecated
| KnownClass::StdlibAlias
| KnownClass::SpecialForm
@@ -691,6 +704,7 @@ impl KnownClass {
| KnownClass::SupportsIndex
| KnownClass::Iterable
| KnownClass::Iterator
+ | KnownClass::AsyncIterator
| KnownClass::Sequence
| KnownClass::Mapping
| KnownClass::ChainMap
@@ -739,6 +753,7 @@ impl KnownClass {
Self::Classmethod => "classmethod",
Self::Awaitable => "Awaitable",
Self::Generator => "Generator",
+ Self::AsyncGenerator => "AsyncGenerator",
Self::Deprecated => "deprecated",
Self::GenericAlias => "GenericAlias",
Self::ModuleType => "ModuleType",
@@ -785,6 +800,7 @@ impl KnownClass {
Self::Super => "super",
Self::Iterable => "Iterable",
Self::Iterator => "Iterator",
+ Self::AsyncIterator => "AsyncIterator",
Self::Sequence => "Sequence",
Self::Mapping => "Mapping",
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
@@ -1125,11 +1141,13 @@ impl KnownClass {
Self::NoneType => KnownModule::Typeshed,
Self::Awaitable
| Self::Generator
+ | Self::AsyncGenerator
| Self::SpecialForm
| Self::TypeVar
| Self::StdlibAlias
| Self::Iterable
| Self::Iterator
+ | Self::AsyncIterator
| Self::Sequence
| Self::Mapping
| Self::ProtocolMeta
@@ -1232,6 +1250,7 @@ impl KnownClass {
| Self::Classmethod
| Self::Awaitable
| Self::Generator
+ | Self::AsyncGenerator
| Self::Deprecated
| Self::GenericAlias
| Self::ModuleType
@@ -1271,6 +1290,7 @@ impl KnownClass {
| Self::InitVar
| Self::Iterable
| Self::Iterator
+ | Self::AsyncIterator
| Self::Sequence
| Self::Mapping
| Self::NamedTupleFallback
@@ -1342,6 +1362,7 @@ impl KnownClass {
| Self::Classmethod
| Self::Awaitable
| Self::Generator
+ | Self::AsyncGenerator
| Self::Deprecated
| Self::TypeVar
| Self::ExtensionsTypeVar
@@ -1365,6 +1386,7 @@ impl KnownClass {
| Self::InitVar
| Self::Iterable
| Self::Iterator
+ | Self::AsyncIterator
| Self::Sequence
| Self::Mapping
| Self::NamedTupleFallback
@@ -1413,6 +1435,7 @@ impl KnownClass {
"classmethod" => &[Self::Classmethod],
"Awaitable" => &[Self::Awaitable],
"Generator" => &[Self::Generator],
+ "AsyncGenerator" => &[Self::AsyncGenerator],
"deprecated" => &[Self::Deprecated],
"GenericAlias" => &[Self::GenericAlias],
"NoneType" => &[Self::NoneType],
@@ -1431,6 +1454,7 @@ impl KnownClass {
"TypeVar" => &[Self::TypeVar, Self::ExtensionsTypeVar],
"Iterable" => &[Self::Iterable],
"Iterator" => &[Self::Iterator],
+ "AsyncIterator" => &[Self::AsyncIterator],
"Sequence" => &[Self::Sequence],
"Mapping" => &[Self::Mapping],
"ParamSpec" => &[Self::ParamSpec, Self::ExtensionsParamSpec],
@@ -1564,6 +1588,7 @@ impl KnownClass {
| Self::Specialization
| Self::Awaitable
| Self::Generator
+ | Self::AsyncGenerator
| Self::Template
| Self::Path => module == self.canonical_module(db),
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
@@ -1576,6 +1601,7 @@ impl KnownClass {
| Self::TypeVarTuple
| Self::Iterable
| Self::Iterator
+ | Self::AsyncIterator
| Self::Sequence
| Self::Mapping
| Self::ProtocolMeta
diff --git a/crates/ty_python_semantic/src/types/diagnostic.rs b/crates/ty_python_semantic/src/types/diagnostic.rs
index 7c04e5a4498d4..e4fec48a2d90b 100644
--- a/crates/ty_python_semantic/src/types/diagnostic.rs
+++ b/crates/ty_python_semantic/src/types/diagnostic.rs
@@ -78,6 +78,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&ISINSTANCE_AGAINST_TYPED_DICT);
registry.register_lint(&INVALID_ARGUMENT_TYPE);
registry.register_lint(&INVALID_RETURN_TYPE);
+ registry.register_lint(&INVALID_YIELD);
registry.register_lint(&INVALID_ASSIGNMENT);
registry.register_lint(&INVALID_AWAIT);
registry.register_lint(&INVALID_BASE);
@@ -940,6 +941,31 @@ declare_lint! {
}
}
+declare_lint! {
+ /// ## What it does
+ /// Detects `yield` and `yield from` expressions where the "yield" or "send" type
+ /// is incompatible with the generator function's annotated return type.
+ ///
+ /// ## Why is this bad?
+ /// Yielding a value of a type that doesn't match the generator's declared yield type,
+ /// or using `yield from` with a sub-iterator whose yield or send type is incompatible,
+ /// is a type error that may cause downstream consumers of the generator to receive
+ /// values of an unexpected type.
+ ///
+ /// ## Examples
+ /// ```python
+ /// from typing import Iterator
+ ///
+ /// def gen() -> Iterator[int]:
+ /// yield "not an int" # error: [invalid-yield]
+ /// ```
+ pub(crate) static INVALID_YIELD = {
+ summary: "detects yield expressions where the \"yield\" or \"send\" type is incompatible with the annotated return type",
+ status: LintStatus::stable("0.0.25"),
+ default_level: Level::Error,
+ }
+}
+
declare_lint! {
/// ## What it does
/// Detects functions with empty bodies that have a non-`None` return type annotation.
@@ -3764,6 +3790,61 @@ pub(super) fn report_invalid_generator_function_return_type(
diag.info(format_args!("See {link} for more details"));
}
+#[derive(Copy, Clone)]
+pub(super) enum GeneratorMismatchKind {
+ YieldType,
+ SendType,
+}
+
+pub(super) fn report_invalid_generator_yield_type(
+ context: &InferContext,
+ object_range: impl Ranged,
+ return_type_span: Option,
+ expected_ty: Type,
+ actual_ty: Type,
+ kind: GeneratorMismatchKind,
+) {
+ let Some(builder) = context.report_lint(&INVALID_YIELD, object_range) else {
+ return;
+ };
+
+ let settings =
+ DisplaySettings::from_possibly_ambiguous_types(context.db(), [expected_ty, actual_ty]);
+ let expected_ty = expected_ty.display_with(context.db(), settings.clone());
+ let actual_ty = actual_ty.display_with(context.db(), settings);
+
+ let (kind_name, title, concise) = match kind {
+ GeneratorMismatchKind::YieldType => (
+ "yield",
+ "Yield expression type does not match annotation",
+ format!("Yield type `{actual_ty}` does not match annotated yield type `{expected_ty}`"),
+ ),
+ GeneratorMismatchKind::SendType => (
+ "send",
+ "Send type does not match annotation",
+ format!("Send type `{actual_ty}` does not match annotated send type `{expected_ty}`"),
+ ),
+ };
+
+ let mut diag = builder.into_diagnostic(title);
+ diag.set_concise_message(concise);
+ let primary = match kind {
+ GeneratorMismatchKind::YieldType => {
+ format!("expression of type `{actual_ty}`, expected `{expected_ty}`")
+ }
+ GeneratorMismatchKind::SendType => {
+ format!("generator with send type `{actual_ty}`, expected `{expected_ty}`")
+ }
+ };
+ diag.set_primary_message(primary);
+
+ if let Some(return_type_span) = return_type_span {
+ diag.annotate(Annotation::secondary(return_type_span).message(format!(
+ "Function annotated with {kind_name} type `{expected_ty}` here"
+ )));
+ }
+}
+
pub(super) fn report_implicit_return_type(
context: &InferContext,
range: impl Ranged,
diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs
index 340f8ccbdcf8c..b6b7f50753c5b 100644
--- a/crates/ty_python_semantic/src/types/infer/builder.rs
+++ b/crates/ty_python_semantic/src/types/infer/builder.rs
@@ -61,22 +61,23 @@ use crate::types::constraints::ConstraintSetBuilder;
use crate::types::context::InferContext;
use crate::types::diagnostic::{
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CYCLIC_CLASS_DEFINITION,
- CYCLIC_TYPE_ALIAS_DEFINITION, DUPLICATE_BASE, INCONSISTENT_MRO, INEFFECTIVE_FINAL,
- INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE,
- INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION, INVALID_LEGACY_TYPE_VARIABLE,
- INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_TYPE_ALIAS_TYPE, INVALID_TYPE_FORM,
- INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_BOUND, INVALID_TYPE_VARIABLE_CONSTRAINTS,
- IncompatibleBases, NO_MATCHING_OVERLOAD, POSSIBLY_MISSING_IMPLICIT_CALL,
- POSSIBLY_MISSING_SUBMODULE, SUBCLASS_OF_FINAL_CLASS, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
- UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE, UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR,
- UNUSED_AWAITABLE, hint_if_stdlib_attribute_exists_on_other_versions,
- report_attempted_protocol_instantiation, report_bad_dunder_set_call,
- report_call_to_abstract_method, report_cannot_pop_required_field_on_typed_dict,
- report_conflicting_metaclass_from_bases, report_instance_layout_conflict,
- report_invalid_assignment, report_invalid_attribute_assignment,
- report_invalid_class_match_pattern, report_invalid_exception_caught,
- report_invalid_exception_cause, report_invalid_exception_raised,
- report_invalid_exception_tuple_caught, report_invalid_key_on_typed_dict,
+ CYCLIC_TYPE_ALIAS_DEFINITION, DUPLICATE_BASE, GeneratorMismatchKind, INCONSISTENT_MRO,
+ INEFFECTIVE_FINAL, INVALID_ARGUMENT_TYPE, INVALID_ASSIGNMENT, INVALID_ATTRIBUTE_ACCESS,
+ INVALID_BASE, INVALID_DECLARATION, INVALID_ENUM_MEMBER_ANNOTATION,
+ INVALID_LEGACY_TYPE_VARIABLE, INVALID_NEWTYPE, INVALID_PARAMSPEC, INVALID_TYPE_ALIAS_TYPE,
+ INVALID_TYPE_FORM, INVALID_TYPE_GUARD_CALL, INVALID_TYPE_VARIABLE_BOUND,
+ INVALID_TYPE_VARIABLE_CONSTRAINTS, IncompatibleBases, NO_MATCHING_OVERLOAD,
+ POSSIBLY_MISSING_IMPLICIT_CALL, POSSIBLY_MISSING_SUBMODULE, SUBCLASS_OF_FINAL_CLASS,
+ UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_GLOBAL, UNRESOLVED_REFERENCE,
+ UNSUPPORTED_DYNAMIC_BASE, UNSUPPORTED_OPERATOR, UNUSED_AWAITABLE,
+ hint_if_stdlib_attribute_exists_on_other_versions, report_attempted_protocol_instantiation,
+ report_bad_dunder_set_call, report_call_to_abstract_method,
+ report_cannot_pop_required_field_on_typed_dict, report_conflicting_metaclass_from_bases,
+ report_instance_layout_conflict, report_invalid_assignment,
+ report_invalid_attribute_assignment, report_invalid_class_match_pattern,
+ report_invalid_exception_caught, report_invalid_exception_cause,
+ report_invalid_exception_raised, report_invalid_exception_tuple_caught,
+ report_invalid_generator_yield_type, report_invalid_key_on_typed_dict,
report_invalid_type_checking_constant,
report_match_pattern_against_non_runtime_checkable_protocol,
report_match_pattern_against_typed_dict, report_possibly_missing_attribute,
@@ -7276,8 +7277,45 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
node_index: _,
value,
} = yield_expression;
- self.infer_optional_expression(value.as_deref(), TypeContext::default());
- todo_type!("yield expressions")
+ let Some(enclosing_function) =
+ nearest_enclosing_function(self.db(), self.index, self.scope())
+ else {
+ let _ = self.infer_optional_expression(value.as_deref(), TypeContext::default());
+ return Type::unknown();
+ };
+ let declared_return_ty = enclosing_function
+ .last_definition_raw_signature(self.db())
+ .return_ty;
+ let return_type_span = enclosing_function.spans(self.db()).return_type;
+
+ let Some(generator_type_params) = declared_return_ty.generator_types(self.db()) else {
+ let _ = self.infer_optional_expression(value.as_deref(), TypeContext::default());
+ return Type::unknown();
+ };
+
+ let expected_yield_ty = generator_type_params.yield_ty;
+ let tcx = TypeContext::new(expected_yield_ty);
+ let yielded_ty = self
+ .infer_optional_expression(value.as_deref(), tcx)
+ .unwrap_or_else(|| Type::none(self.db()));
+ let diagnostic_node: AnyNodeRef = value
+ .as_deref()
+ .map_or_else(|| yield_expression.into(), AnyNodeRef::from);
+
+ if let Some(expected_yield_ty) = expected_yield_ty
+ && !yielded_ty.is_assignable_to(self.db(), expected_yield_ty)
+ {
+ report_invalid_generator_yield_type(
+ &self.context,
+ diagnostic_node,
+ return_type_span,
+ expected_yield_ty,
+ yielded_ty,
+ GeneratorMismatchKind::YieldType,
+ );
+ }
+
+ generator_type_params.send_ty.unwrap_or_else(Type::unknown)
}
fn infer_yield_from_expression(&mut self, yield_from: &ast::ExprYieldFrom) -> Type<'db> {
@@ -7287,8 +7325,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
value,
} = yield_from;
- let iterable_type = self.infer_expression(value, TypeContext::default());
- iterable_type
+ let Some(enclosing_function) =
+ nearest_enclosing_function(self.db(), self.index, self.scope())
+ else {
+ let _ = self.infer_expression(value, TypeContext::default());
+ return Type::unknown();
+ };
+ let annotated_return_ty = enclosing_function
+ .last_definition_raw_signature(self.db())
+ .return_ty;
+
+ let Some(outer_expected) = annotated_return_ty.generator_types(self.db()) else {
+ let _ = self.infer_expression(value, TypeContext::default());
+ return Type::unknown();
+ };
+ let return_type_span = enclosing_function.spans(self.db()).return_type;
+
+ let tcx = TypeContext::new(outer_expected.yield_ty.map(|yielded_ty| {
+ KnownClass::Iterable.to_specialized_instance(self.db(), &[yielded_ty])
+ }));
+ let iterable_type = self.infer_expression(value, tcx);
+
+ let inner_yield_ty = iterable_type
.try_iterate(self.db())
.map(|tuple| tuple.homogeneous_element_type(self.db()))
.unwrap_or_else(|err| {
@@ -7296,6 +7354,35 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
err.fallback_element_type(self.db())
});
+ if let Some(outer_yield_ty) = outer_expected.yield_ty
+ && !inner_yield_ty.is_assignable_to(self.db(), outer_yield_ty)
+ {
+ report_invalid_generator_yield_type(
+ &self.context,
+ value.as_ref(),
+ return_type_span.clone(),
+ outer_yield_ty,
+ inner_yield_ty,
+ GeneratorMismatchKind::YieldType,
+ );
+ }
+
+ if let Some(outer_send_ty) = outer_expected.send_ty {
+ let inner_send_ty = iterable_type
+ .generator_send_type(self.db())
+ .unwrap_or_else(|| Type::none(self.db()));
+ if !outer_send_ty.is_assignable_to(self.db(), inner_send_ty) {
+ report_invalid_generator_yield_type(
+ &self.context,
+ value.as_ref(),
+ return_type_span,
+ outer_send_ty,
+ inner_send_ty,
+ GeneratorMismatchKind::SendType,
+ );
+ }
+ }
+
iterable_type
.generator_return_type(self.db())
.unwrap_or_else(Type::unknown)
diff --git a/ty.schema.json b/ty.schema.json
index 9ff5db3cbae2c..6614c168ddb37 100644
--- a/ty.schema.json
+++ b/ty.schema.json
@@ -1094,6 +1094,16 @@
}
]
},
+ "invalid-yield": {
+ "title": "detects yield expressions where the \"yield\" or \"send\" type is incompatible with the annotated return type",
+ "description": "## What it does\nDetects `yield` and `yield from` expressions where the \"yield\" or \"send\" type\nis incompatible with the generator function's annotated return type.\n\n## Why is this bad?\nYielding a value of a type that doesn't match the generator's declared yield type,\nor using `yield from` with a sub-iterator whose yield or send type is incompatible,\nis a type error that may cause downstream consumers of the generator to receive\nvalues of an unexpected type.\n\n## Examples\n```python\nfrom typing import Iterator\n\ndef gen() -> Iterator[int]:\n yield \"not an int\" # error: [invalid-yield]\n```",
+ "default": "error",
+ "oneOf": [
+ {
+ "$ref": "#/definitions/Level"
+ }
+ ]
+ },
"isinstance-against-protocol": {
"title": "reports invalid runtime checks against protocol classes",
"description": "## What it does\nReports invalid runtime checks against `Protocol` classes.\nThis includes explicit calls `isinstance()`/`issubclass()` against\nnon-runtime-checkable protocols, `issubclass()` calls against protocols\nthat have non-method members, and implicit `isinstance()` checks against\nnon-runtime-checkable protocols via pattern matching.\n\n## Why is this bad?\nThese calls (implicit or explicit) raise `TypeError` at runtime.\n\n## Examples\n```python\nfrom typing_extensions import Protocol, runtime_checkable\n\nclass HasX(Protocol):\n x: int\n\n@runtime_checkable\nclass HasY(Protocol):\n y: int\n\ndef f(arg: object, arg2: type):\n isinstance(arg, HasX) # error: [isinstance-against-protocol] (not runtime-checkable)\n issubclass(arg2, HasX) # error: [isinstance-against-protocol] (not runtime-checkable)\n\ndef g(arg: object):\n match arg:\n case HasX(): # error: [isinstance-against-protocol] (not runtime-checkable)\n pass\n\ndef h(arg2: type):\n isinstance(arg2, HasY) # fine (runtime-checkable)\n\n # `HasY` is runtime-checkable, but has non-method members,\n # so it still can't be used in `issubclass` checks)\n issubclass(arg2, HasY) # error: [isinstance-against-protocol]\n```\n\n## References\n- [Typing documentation: `@runtime_checkable`](https://docs.python.org/3/library/typing.html#typing.runtime_checkable)",