Repository: pylint-dev/pylint

Issue:
False positive for `use-yield-from` for generators yielding from iterators
### Bug description

Within a generator expression, pylint issues `use-yield-from` for constructs such as:

```python
def my_generator[T, S](iterator: Iterable[T]) -> Generator[T, S, S]:
    for item in iterator:
        state = yield iterator
    return state
```

When `iterator` is an `Iterator` and not a `Generator`, it will not have the `generator` methods (e.g. `send`), so applying Pylint's suggestion will modify the type of `my_generator` from `Generator[T, S, S]` to `Generator[T, None, None]` (and will generally cause `AttributeError` to be raised on calls to `send`).

Example file `generator.py`:
```python
import typing

iterable: list[int] = [1, 2, 3]

def gen1():
    for item in iterable:
        # Implicitly suggesting that `send` will be used.
        _ = yield item

# ... and ...

# Explicitly requiring that `send` must exist.
def gen2() -> typing.Generator[int, object, None]:
    for item in iterable:
        yield item
```


### Configuration

_No response_

### Command used

```shell
pylint generator.py
```


### Pylint output

```python
************* Module generator
generator.py:6:4: R1737: Use 'yield from' directly instead of yielding each element one by one (use-yield-from)
generator.py:14:4: R1737: Use 'yield from' directly instead of yielding each element one by one (use-yield-from)
```


### Expected behavior

In many cases, I suspect that the message is worth the risk of the false positive:
```python
def gen():
    for item in iterator:
        yield item
```
probably should emit the message, but imo neither of my examples in `generator.py` should, maybe unless `iterable` could be inferred to be a compatible type (which it explicitly isn't in my example).

### Pylint version

```shell
pylint 3.2.2
astroid 3.2.2
Python 3.12.0rc3 (main, Sep 21 2023, 17:27:59) [Clang 16.0.6 ]
```


### OS / Environment

_No response_

### Additional dependencies

_No response_


Discussion:

Patch:
diff --git a/doc/whatsnew/fragments/9696.false_positive b/doc/whatsnew/fragments/9696.false_positive
new file mode 100644
index 0000000000..5d6e8cbf11
--- /dev/null
+++ b/doc/whatsnew/fragments/9696.false_positive
@@ -0,0 +1,3 @@
+Fix a false positive for ``use-yield-from`` when using the return value from the ``yield`` atom.
+
+Closes #9696
diff --git a/pylint/checkers/refactoring/refactoring_checker.py b/pylint/checkers/refactoring/refactoring_checker.py
index 94e722b177..9ffcecce12 100644
--- a/pylint/checkers/refactoring/refactoring_checker.py
+++ b/pylint/checkers/refactoring/refactoring_checker.py
@@ -1169,21 +1169,24 @@ def visit_yield(self, node: nodes.Yield) -> None:
         if not isinstance(node.value, nodes.Name):
             return
 
-        parent = node.parent.parent
+        loop_node = node.parent.parent
         if (
-            not isinstance(parent, nodes.For)
-            or isinstance(parent, nodes.AsyncFor)
-            or len(parent.body) != 1
+            not isinstance(loop_node, nodes.For)
+            or isinstance(loop_node, nodes.AsyncFor)
+            or len(loop_node.body) != 1
+            # Avoid a false positive if the return value from `yield` is used,
+            # (such as via Assign, AugAssign, etc).
+            or not isinstance(node.parent, nodes.Expr)
         ):
             return
 
-        if parent.target.name != node.value.name:
+        if loop_node.target.name != node.value.name:
             return
 
         if isinstance(node.frame(), nodes.AsyncFunctionDef):
             return
 
-        self.add_message("use-yield-from", node=parent, confidence=HIGH)
+        self.add_message("use-yield-from", node=loop_node, confidence=HIGH)
 
     @staticmethod
     def _has_exit_in_scope(scope: nodes.LocalsDictNodeNG) -> bool:


Test patch:
diff --git a/tests/functional/u/use/use_yield_from.py b/tests/functional/u/use/use_yield_from.py
index 2ccbb6d77e..f4dcdc6b9e 100644
--- a/tests/functional/u/use/use_yield_from.py
+++ b/tests/functional/u/use/use_yield_from.py
@@ -57,3 +57,13 @@ async def async_for_yield(agen):
 async def async_yield(agen):
     for item in agen:
         yield item
+
+
+# If the return from `yield` is used inline, don't suggest delegation.
+
+def yield_use_send():
+    for item in (1, 2, 3):
+        _ = yield item
+    total = 0
+    for item in (1, 2, 3):
+        total += yield item