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)",