Repository: pylint-dev/astroid

Issue:
false not-an-iterable for class method with @overload (2.5.7 regression)
### Steps to reproduce

Starting from astroid 2.5.7, I'm seeing false not-an-iterable when generator class methods are annotated with `@overload`.

```python
from typing import overload, Iterator

class MyClass:
    @overload
    def transitions(self, foo: int, bar: int) -> Iterator[int]: ...
    @overload
    def transitions(self, baz: str) -> Iterator[str]: ...
    def transitions(self, foo_or_baz, bar=None):
        yield

for _ in MyClass().transitions('hello'):
    pass
```

If `@overload` is removed, or the function is moved to the module level, or I switch to astroid 2.5.6, the problem goes away.

It happens with pylint-2.8.3 or pylint-3.0.0a3.

### Current behavior

E1133: Non-iterable value MyClass().transitions('hello') is used in an iterating context (not-an-iterable)

### Expected behavior

no error



Discussion / hints:
I can work around it by replacing `...` with `yield`, but it doesn't seem that this should be necessary (and it wasn't in prior versions of Astroid).  And it is hard to satisfy mypy with this workaround, because it expects `yield [value]` etc.
@belm0 thanks for the report.
The issue seems to have started with #934, the previous commit is fine: https://github.com/PyCQA/astroid/commit/2e8417ffc2285e798ccdef86f743abb75958e2c6. (Tested with pylint `2.8.3`.)
@nelfin Would you like to take a look at this?
Sure, happy to take this one



Fix (patch):
diff --git a/ChangeLog b/ChangeLog
index 86b614c1f6..ac5df56f4f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -7,6 +7,11 @@ What's New in astroid 3.2.0?
 ============================
 Release date: TBA
 
+* ``igetattr()`` returns the last same-named function in a class (instead of
+  the first). This avoids false positives in pylint with ``@overload``.
+
+  Closes #1015
+  Refs pylint-dev/pylint#4696
 
 
 What's New in astroid 3.1.1?
diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py
index 9cda4f1be0..79b7643e55 100644
--- a/astroid/nodes/scoped_nodes/scoped_nodes.py
+++ b/astroid/nodes/scoped_nodes/scoped_nodes.py
@@ -2508,12 +2508,21 @@ def igetattr(
             # to the attribute happening *after* the attribute's definition (e.g. AugAssigns on lists)
             if len(attributes) > 1:
                 first_attr, attributes = attributes[0], attributes[1:]
-                first_scope = first_attr.scope()
+                first_scope = first_attr.parent.scope()
                 attributes = [first_attr] + [
                     attr
                     for attr in attributes
                     if attr.parent and attr.parent.scope() == first_scope
                 ]
+            functions = [attr for attr in attributes if isinstance(attr, FunctionDef)]
+            if functions:
+                # Prefer only the last function, unless a property is involved.
+                last_function = functions[-1]
+                attributes = [
+                    a
+                    for a in attributes
+                    if a not in functions or a is last_function or bases._is_property(a)
+                ]
 
             for inferred in bases._infer_stmts(attributes, context, frame=self):
                 # yield Uninferable object instead of descriptors when necessary


Test changes:
diff --git a/tests/test_inference.py b/tests/test_inference.py
index ffd78fe035..10fceb7b56 100644
--- a/tests/test_inference.py
+++ b/tests/test_inference.py
@@ -30,7 +30,7 @@
 )
 from astroid import decorators as decoratorsmod
 from astroid.arguments import CallSite
-from astroid.bases import BoundMethod, Instance, UnboundMethod, UnionType
+from astroid.bases import BoundMethod, Generator, Instance, UnboundMethod, UnionType
 from astroid.builder import AstroidBuilder, _extract_single_node, extract_node, parse
 from astroid.const import IS_PYPY, PY39_PLUS, PY310_PLUS, PY312_PLUS
 from astroid.context import CallContext, InferenceContext
@@ -4321,6 +4321,53 @@ class Test(Outer.Inner):
         assert isinstance(inferred, nodes.Const)
         assert inferred.value == 123
 
+    def test_infer_method_empty_body(self) -> None:
+        # https://github.com/PyCQA/astroid/issues/1015
+        node = extract_node(
+            """
+            class A:
+                def foo(self): ...
+
+            A().foo()  #@
+        """
+        )
+        inferred = next(node.infer())
+        assert isinstance(inferred, nodes.Const)
+        assert inferred.value is None
+
+    def test_infer_method_overload(self) -> None:
+        # https://github.com/PyCQA/astroid/issues/1015
+        node = extract_node(
+            """
+            class A:
+                def foo(self): ...
+
+                def foo(self):
+                    yield
+
+            A().foo()  #@
+        """
+        )
+        inferred = list(node.infer())
+        assert len(inferred) == 1
+        assert isinstance(inferred[0], Generator)
+
+    def test_infer_function_under_if(self) -> None:
+        node = extract_node(
+            """
+        if 1 in [1]:
+            def func():
+                return 42
+        else:
+            def func():
+                return False
+
+        func()  #@
+        """
+        )
+        inferred = list(node.inferred())
+        assert [const.value for const in inferred] == [42, False]
+
     def test_delayed_attributes_without_slots(self) -> None:
         ast_node = extract_node(
             """