Repository: pylint-dev/astroid

Issue:
Cannot infer empty functions
### Steps to reproduce
```python
import astroid
astroid.extract_node("""
def f():
    pass
f()
""").inferred()
```
### Current behavior
raises `StopIteration`

### Expected behavior
Returns `[const.NoneType]`

### ``python -c "from astroid import __pkginfo__; print(__pkginfo__.version)"`` output

2.0.0

This also applies to procedural functions which don't explicitly return any values.


Discussion:
Makes sense.

This happens because the inference for functions looks through each return value here (https://github.com/PyCQA/astroid/blob/629c92db2dc1b016f4bf47645c95c42e65fd3bd6/astroid/scoped_nodes.py#L1558) and tries to infer the result from there. But since functions like this don't have an explicit return value, the inference gets into `raise_if_nothing_inferred` over here (https://github.com/PyCQA/astroid/blob/ac3e82e9bd8678086325a71a927a06bbc43d415e/astroid/decorators.py#L140), resulting in the exception you see. 
What should we infer for a function that always raises an exception? I don't think it should be `None`. Uninferable I guess?

---

I tried to add this, but It caused a cascade of errors where we are looking for Uninferable instead of const.None
@brycepg I would say it should return `Uninferable`, as raising exceptions is not necessarily returning a value from the function. Although we'd want some mechanism to get what exceptions a function could raise.

Regarding your last statement, you mean you added `Uninferable` for functions that raise exceptions or for functions that return `None`? Which of these failed with the cascade of errors?
@PCManticore I made functions that do not have any return/yield nodes infer to `None` instead of `Uninferable`, and it broke a lot of tests.

Patch:
diff --git a/astroid/bases.py b/astroid/bases.py
--- a/astroid/bases.py
+++ b/astroid/bases.py
@@ -329,6 +329,12 @@ def getitem(self, index, context=None):
             raise exceptions.InferenceError(
                 "Could not find __getitem__ for {node!r}.", node=self, context=context
             )
+        if len(method.args.arguments) != 2:  # (self, index)
+            raise exceptions.AstroidTypeError(
+                "__getitem__ for {node!r} does not have correct signature",
+                node=self,
+                context=context,
+            )
         return next(method.infer_call_result(self, new_context))
 
 
diff --git a/astroid/scoped_nodes.py b/astroid/scoped_nodes.py
--- a/astroid/scoped_nodes.py
+++ b/astroid/scoped_nodes.py
@@ -1661,11 +1661,12 @@ def is_bound(self):
         """
         return self.type == "classmethod"
 
-    def is_abstract(self, pass_is_abstract=True):
+    def is_abstract(self, pass_is_abstract=True, any_raise_is_abstract=False):
         """Check if the method is abstract.
 
         A method is considered abstract if any of the following is true:
         * The only statement is 'raise NotImplementedError'
+        * The only statement is 'raise <SomeException>' and any_raise_is_abstract is True
         * The only statement is 'pass' and pass_is_abstract is True
         * The method is annotated with abc.astractproperty/abc.abstractmethod
 
@@ -1686,6 +1687,8 @@ def is_abstract(self, pass_is_abstract=True):
 
         for child_node in self.body:
             if isinstance(child_node, node_classes.Raise):
+                if any_raise_is_abstract:
+                    return True
                 if child_node.raises_not_implemented():
                     return True
             return pass_is_abstract and isinstance(child_node, node_classes.Pass)
@@ -1744,8 +1747,11 @@ def infer_call_result(self, caller=None, context=None):
 
         first_return = next(returns, None)
         if not first_return:
-            if self.body and isinstance(self.body[-1], node_classes.Assert):
-                yield node_classes.Const(None)
+            if self.body:
+                if self.is_abstract(pass_is_abstract=True, any_raise_is_abstract=True):
+                    yield util.Uninferable
+                else:
+                    yield node_classes.Const(None)
                 return
 
             raise exceptions.InferenceError(


Test patch:
diff --git a/tests/unittest_inference.py b/tests/unittest_inference.py
--- a/tests/unittest_inference.py
+++ b/tests/unittest_inference.py
@@ -706,14 +706,6 @@ class InvalidGetitem2(object):
         NoGetitem()[4] #@
         InvalidGetitem()[5] #@
         InvalidGetitem2()[10] #@
-        """
-        )
-        for node in ast_nodes[:3]:
-            self.assertRaises(InferenceError, next, node.infer())
-        for node in ast_nodes[3:]:
-            self.assertEqual(next(node.infer()), util.Uninferable)
-        ast_nodes = extract_node(
-            """
         [1, 2, 3][None] #@
         'lala'['bala'] #@
         """
@@ -5404,26 +5396,25 @@ class Cls:
 def test_prevent_recursion_error_in_igetattr_and_context_manager_inference():
     code = """
     class DummyContext(object):
-        def method(self, msg): # pylint: disable=C0103
-            pass
         def __enter__(self):
-            pass
+            return self
         def __exit__(self, ex_type, ex_value, ex_tb):
             return True
 
-    class CallMeMaybe(object):
-        def __call__(self):
-            while False:
-                with DummyContext() as con:
-                    f_method = con.method
-                break
+    if False:
+        with DummyContext() as con:
+            pass
 
-            with DummyContext() as con:
-                con #@
-                f_method = con.method
+    with DummyContext() as con:
+        con.__enter__  #@
     """
     node = extract_node(code)
-    assert next(node.infer()) is util.Uninferable
+    # According to the original issue raised that introduced this test
+    # (https://github.com/PyCQA/astroid/663, see 55076ca), this test was a
+    # non-regression check for StopIteration leaking out of inference and
+    # causing a RuntimeError. Hence, here just consume the inferred value
+    # without checking it and rely on pytest to fail on raise
+    next(node.infer())
 
 
 def test_infer_context_manager_with_unknown_args():
diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py
--- a/tests/unittest_scoped_nodes.py
+++ b/tests/unittest_scoped_nodes.py
@@ -466,6 +466,55 @@ def func():
         self.assertIsInstance(func_vals[0], nodes.Const)
         self.assertIsNone(func_vals[0].value)
 
+    def test_no_returns_is_implicitly_none(self):
+        code = """
+            def f():
+                print('non-empty, non-pass, no return statements')
+            value = f()
+            value
+        """
+        node = builder.extract_node(code)
+        inferred = next(node.infer())
+        assert isinstance(inferred, nodes.Const)
+        assert inferred.value is None
+
+    def test_only_raises_is_not_implicitly_none(self):
+        code = """
+            def f():
+                raise SystemExit()
+            f()
+        """
+        node = builder.extract_node(code)  # type: nodes.Call
+        inferred = next(node.infer())
+        assert inferred is util.Uninferable
+
+    def test_abstract_methods_are_not_implicitly_none(self):
+        code = """
+            from abc import ABCMeta, abstractmethod
+
+            class Abstract(metaclass=ABCMeta):
+                @abstractmethod
+                def foo(self):
+                    pass
+                def bar(self):
+                    print('non-empty, non-pass, no return statements')
+            Abstract().foo()  #@
+            Abstract().bar()  #@
+
+            class Concrete(Abstract):
+                def foo(self):
+                    return 123
+            Concrete().foo()  #@
+            Concrete().bar()  #@
+        """
+        afoo, abar, cfoo, cbar = builder.extract_node(code)
+
+        assert next(afoo.infer()) is util.Uninferable
+        for node, value in [(abar, None), (cfoo, 123), (cbar, None)]:
+            inferred = next(node.infer())
+            assert isinstance(inferred, nodes.Const)
+            assert inferred.value == value
+
     def test_func_instance_attr(self):
         """test instance attributes for functions"""
         data = """