Repository: pylint-dev/astroid

Issue:
Incorrect inference of `self` in methods defined in superclasses in unbound methods on a subclass
### Steps to reproduce
```python
import astroid
node = astroid.extract_node("""
class Base:
    def replace(self):
        return self

class Derived(Base):
    def method(self):
        return 123

this = Derived()
that = Derived.replace(this)
that
""")
inferred = next(node.infer())
inferred.getattr('method')
```

### Current behavior

`that` is inferred as an instance of `.Base` not `.Derived`, causing this exception:

```python
Traceback (most recent call last):
  File "astroid/bases.py", line 182, in getattr
    values = self._proxied.instance_attr(name, context)
  File "astroid/scoped_nodes.py", line 2433, in instance_attr
    raise exceptions.AttributeInferenceError(
astroid.exceptions.AttributeInferenceError: 'method' not found on <ClassDef.Base l.2 at 0x...>.
```

which leads to `no-member` warnings on `pylint`

### Expected behavior

`that` is inferred as an instance of `.Derived`.

### `python -c "from astroid import __pkginfo__; print(__pkginfo__.version)"` output

2.5.7

### Related issues

Appears to be the root-cause of PyCQA/pylint#4377.
Ref PyCQA/pylint#4487


Discussion / hints:

Fix (patch):
diff --git a/ChangeLog b/ChangeLog
index a8bf1711cc..b79ee7c264 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -6,6 +6,20 @@ What's New in astroid 2.8.0?
 ============================
 Release date: TBA
 
+* Fixed bug in attribute inference from inside method calls.
+
+    Closes PyCQA/pylint#400
+
+* Fixed bug in inference for superclass instance methods called
+  from the class rather than an instance.
+
+    Closes #1008
+    Closes PyCQA/pylint#4377
+
+* Fixed bug in inference of chained attributes where a subclass
+  had an attribute that was an instance of its superclass.
+
+    Closes PyCQA/pylint#4220
 
 
 What's New in astroid 2.7.3?
diff --git a/astroid/arguments.py b/astroid/arguments.py
index 3ee77ef9f4..a1104e2b67 100644
--- a/astroid/arguments.py
+++ b/astroid/arguments.py
@@ -216,11 +216,15 @@ def infer_argument(self, funcnode, name, context):
                     positional.append(arg)
 
         if argindex is not None:
+            boundnode = getattr(context, "boundnode", None)
             # 2. first argument of instance/class method
             if argindex == 0 and funcnode.type in ("method", "classmethod"):
-                if context.boundnode is not None:
-                    boundnode = context.boundnode
-                else:
+                # context.boundnode is None when an instance method is called with
+                # the class, e.g. MyClass.method(obj, ...). In this case, self
+                # is the first argument.
+                if boundnode is None and funcnode.type == "method" and positional:
+                    return positional[0].infer(context=context)
+                if boundnode is None:
                     # XXX can do better ?
                     boundnode = funcnode.parent.frame()
 
@@ -242,7 +246,7 @@ def infer_argument(self, funcnode, name, context):
             # if we have a method, extract one position
             # from the index, so we'll take in account
             # the extra parameter represented by `self` or `cls`
-            if funcnode.type in ("method", "classmethod"):
+            if funcnode.type in ("method", "classmethod") and boundnode:
                 argindex -= 1
             # 2. search arg index
             try:
diff --git a/astroid/bases.py b/astroid/bases.py
index bef2947f9e..554da766f6 100644
--- a/astroid/bases.py
+++ b/astroid/bases.py
@@ -169,6 +169,7 @@ def _infer_method_result_truth(instance, method_name, context):
         if not meth.callable():
             return util.Uninferable
         try:
+            context.callcontext = contextmod.CallContext(args=[], callee=meth)
             for value in meth.infer_call_result(instance, context=context):
                 if value is util.Uninferable:
                     return value
@@ -318,7 +319,6 @@ def bool_value(self, context=None):
              all its instances are considered true.
         """
         context = context or contextmod.InferenceContext()
-        context.callcontext = contextmod.CallContext(args=[])
         context.boundnode = self
 
         try:
@@ -336,9 +336,9 @@ def getitem(self, index, context=None):
         new_context = contextmod.bind_context_to_node(context, self)
         if not context:
             context = new_context
-        # Create a new CallContext for providing index as an argument.
-        new_context.callcontext = contextmod.CallContext(args=[index])
         method = next(self.igetattr("__getitem__", context=context), None)
+        # Create a new CallContext for providing index as an argument.
+        new_context.callcontext = contextmod.CallContext(args=[index], callee=method)
         if not isinstance(method, BoundMethod):
             raise InferenceError(
                 "Could not find __getitem__ for {node!r}.", node=self, context=context
diff --git a/astroid/context.py b/astroid/context.py
index 6cb4cd641f..ecedd227cc 100644
--- a/astroid/context.py
+++ b/astroid/context.py
@@ -14,10 +14,10 @@
 """Various context related utilities, including inference and call contexts."""
 import contextlib
 import pprint
-from typing import TYPE_CHECKING, MutableMapping, Optional, Sequence, Tuple
+from typing import TYPE_CHECKING, List, MutableMapping, Optional, Sequence, Tuple
 
 if TYPE_CHECKING:
-    from astroid.nodes.node_classes import NodeNG
+    from astroid.nodes.node_classes import Keyword, NodeNG
 
 
 _INFERENCE_CACHE = {}
@@ -164,19 +164,21 @@ def __str__(self):
 class CallContext:
     """Holds information for a call site."""
 
-    __slots__ = ("args", "keywords")
+    __slots__ = ("args", "keywords", "callee")
 
-    def __init__(self, args, keywords=None):
-        """
-        :param List[NodeNG] args: Call positional arguments
-        :param Union[List[nodes.Keyword], None] keywords: Call keywords
-        """
-        self.args = args
+    def __init__(
+        self,
+        args: List["NodeNG"],
+        keywords: Optional[List["Keyword"]] = None,
+        callee: Optional["NodeNG"] = None,
+    ):
+        self.args = args  # Call positional arguments
         if keywords:
             keywords = [(arg.arg, arg.value) for arg in keywords]
         else:
             keywords = []
-        self.keywords = keywords
+        self.keywords = keywords  # Call keyword arguments
+        self.callee = callee  # Function being called
 
 
 def copy_context(context: Optional[InferenceContext]) -> InferenceContext:
diff --git a/astroid/helpers.py b/astroid/helpers.py
index f0fbedb6b1..586cf3a99f 100644
--- a/astroid/helpers.py
+++ b/astroid/helpers.py
@@ -219,13 +219,14 @@ def class_instance_as_index(node):
     for instance when multiplying or subscripting a list.
     """
     context = contextmod.InferenceContext()
-    context.callcontext = contextmod.CallContext(args=[node])
 
     try:
         for inferred in node.igetattr("__index__", context=context):
             if not isinstance(inferred, bases.BoundMethod):
                 continue
 
+            context.boundnode = node
+            context.callcontext = contextmod.CallContext(args=[], callee=inferred)
             for result in inferred.infer_call_result(node, context=context):
                 if isinstance(result, nodes.Const) and isinstance(result.value, int):
                     return result
diff --git a/astroid/inference.py b/astroid/inference.py
index 80f7860d36..2c4b8c94df 100644
--- a/astroid/inference.py
+++ b/astroid/inference.py
@@ -223,9 +223,6 @@ def infer_name(self, context=None):
 def infer_call(self, context=None):
     """infer a Call node by trying to guess what the function returns"""
     callcontext = contextmod.copy_context(context)
-    callcontext.callcontext = contextmod.CallContext(
-        args=self.args, keywords=self.keywords
-    )
     callcontext.boundnode = None
     if context is not None:
         callcontext.extra_context = _populate_context_lookup(self, context.clone())
@@ -236,6 +233,9 @@ def infer_call(self, context=None):
             continue
         try:
             if hasattr(callee, "infer_call_result"):
+                callcontext.callcontext = contextmod.CallContext(
+                    args=self.args, keywords=self.keywords, callee=callee
+                )
                 yield from callee.infer_call_result(caller=self, context=callcontext)
         except InferenceError:
             continue
@@ -304,23 +304,7 @@ def infer_attribute(self, context=None):
             yield owner
             continue
 
-        if context and context.boundnode:
-            # This handles the situation where the attribute is accessed through a subclass
-            # of a base class and the attribute is defined at the base class's level,
-            # by taking in consideration a redefinition in the subclass.
-            if isinstance(owner, bases.Instance) and isinstance(
-                context.boundnode, bases.Instance
-            ):
-                try:
-                    if helpers.is_subtype(
-                        helpers.object_type(context.boundnode),
-                        helpers.object_type(owner),
-                    ):
-                        owner = context.boundnode
-                except _NonDeducibleTypeHierarchy:
-                    # Can't determine anything useful.
-                    pass
-        elif not context:
+        if not context:
             context = contextmod.InferenceContext()
 
         old_boundnode = context.boundnode
@@ -535,7 +519,10 @@ def _infer_unaryop(self, context=None):
                         continue
 
                     context = contextmod.copy_context(context)
-                    context.callcontext = contextmod.CallContext(args=[operand])
+                    context.boundnode = operand
+                    context.callcontext = contextmod.CallContext(
+                        args=[], callee=inferred
+                    )
                     call_results = inferred.infer_call_result(self, context=context)
                     result = next(call_results, None)
                     if result is None:
@@ -574,6 +561,7 @@ def _invoke_binop_inference(instance, opnode, op, other, context, method_name):
     methods = dunder_lookup.lookup(instance, method_name)
     context = contextmod.bind_context_to_node(context, instance)
     method = methods[0]
+    context.callcontext.callee = method
     try:
         inferred = next(method.infer(context=context))
     except StopIteration as e:
diff --git a/astroid/nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes.py
index 40eac1c5f4..3d270e236d 100644
--- a/astroid/nodes/scoped_nodes.py
+++ b/astroid/nodes/scoped_nodes.py
@@ -2267,6 +2267,7 @@ def infer_call_result(self, caller, context=None):
             # Call type.__call__ if not set metaclass
             # (since type is the default metaclass)
             context = contextmod.bind_context_to_node(context, self)
+            context.callcontext.callee = dunder_call
             yield from dunder_call.infer_call_result(caller, context)
         else:
             yield self.instantiate_class()
@@ -2712,7 +2713,7 @@ def getitem(self, index, context=None):
 
         # Create a new callcontext for providing index as an argument.
         new_context = contextmod.bind_context_to_node(context, self)
-        new_context.callcontext = contextmod.CallContext(args=[index])
+        new_context.callcontext = contextmod.CallContext(args=[index], callee=method)
 
         try:
             return next(method.infer_call_result(self, new_context), util.Uninferable)
diff --git a/astroid/protocols.py b/astroid/protocols.py
index 3ae9204df0..57fd1beddc 100644
--- a/astroid/protocols.py
+++ b/astroid/protocols.py
@@ -350,9 +350,13 @@ def _arguments_infer_argname(self, name, context):
             return
 
     if context and context.callcontext:
-        call_site = arguments.CallSite(context.callcontext, context.extra_context)
-        yield from call_site.infer_argument(self.parent, name, context)
-        return
+        callee = context.callcontext.callee
+        while hasattr(callee, "_proxied"):
+            callee = callee._proxied
+        if getattr(callee, "name", None) == self.parent.name:
+            call_site = arguments.CallSite(context.callcontext, context.extra_context)
+            yield from call_site.infer_argument(self.parent, name, context)
+            return
 
     if name == self.vararg:
         vararg = nodes.const_factory(())
@@ -379,6 +383,16 @@ def _arguments_infer_argname(self, name, context):
 
 def arguments_assigned_stmts(self, node=None, context=None, assign_path=None):
     if context.callcontext:
+        callee = context.callcontext.callee
+        while hasattr(callee, "_proxied"):
+            callee = callee._proxied
+    else:
+        callee = None
+    if (
+        context.callcontext
+        and node
+        and getattr(callee, "name", None) == node.frame().name
+    ):
         # reset call context/name
         callcontext = context.callcontext
         context = contextmod.copy_context(context)


Test changes:
diff --git a/tests/unittest_inference.py b/tests/unittest_inference.py
index eac809e4e6..8653fab6e0 100644
--- a/tests/unittest_inference.py
+++ b/tests/unittest_inference.py
@@ -2384,14 +2384,27 @@ class LambdaInstance(object):
             __neg__ = lambda self: self.lala + 1
             @property
             def lala(self): return 24
+        class InstanceWithAttr(object):
+            def __init__(self):
+                self.x = 42
+            def __pos__(self):
+                return self.x
+            def __neg__(self):
+                return +self - 41
+            def __invert__(self):
+                return self.x + 1
         instance = GoodInstance()
         lambda_instance = LambdaInstance()
+        instance_with_attr = InstanceWithAttr()
         +instance #@
         -instance #@
         ~instance #@
         --instance #@
         +lambda_instance #@
         -lambda_instance #@
+        +instance_with_attr #@
+        -instance_with_attr #@
+        ~instance_with_attr #@
 
         bad_instance = BadInstance()
         +bad_instance #@
@@ -2405,13 +2418,13 @@ def lala(self): return 24
         +BadInstance #@
         """
         )
-        expected = [42, 1, 42, -1, 24, 25]
-        for node, value in zip(ast_nodes[:6], expected):
+        expected = [42, 1, 42, -1, 24, 25, 42, 1, 43]
+        for node, value in zip(ast_nodes[:9], expected):
             inferred = next(node.infer())
             self.assertIsInstance(inferred, nodes.Const)
             self.assertEqual(inferred.value, value)
 
-        for bad_node in ast_nodes[6:]:
+        for bad_node in ast_nodes[9:]:
             inferred = next(bad_node.infer())
             self.assertEqual(inferred, util.Uninferable)
 
diff --git a/tests/unittest_inference_calls.py b/tests/unittest_inference_calls.py
new file mode 100644
index 0000000000..c14a8619a8
--- /dev/null
+++ b/tests/unittest_inference_calls.py
@@ -0,0 +1,566 @@
+"""Tests for function call inference"""
+
+from astroid import bases, builder, nodes
+from astroid.util import Uninferable
+
+
+def test_no_return():
+    """Test function with no return statements"""
+    node = builder.extract_node(
+        """
+    def f():
+        pass
+
+    f()  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert inferred[0] is Uninferable
+
+
+def test_one_return():
+    """Test function with a single return that always executes"""
+    node = builder.extract_node(
+        """
+    def f():
+        return 1
+
+    f()  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], nodes.Const)
+    assert inferred[0].value == 1
+
+
+def test_one_return_possible():
+    """Test function with a single return that only sometimes executes
+
+    Note: currently, inference doesn't handle this type of control flow
+    """
+    node = builder.extract_node(
+        """
+    def f(x):
+        if x:
+            return 1
+
+    f(1)  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], nodes.Const)
+    assert inferred[0].value == 1
+
+
+def test_multiple_returns():
+    """Test function with multiple returns"""
+    node = builder.extract_node(
+        """
+    def f(x):
+        if x > 10:
+            return 1
+        elif x > 20:
+            return 2
+        else:
+            return 3
+
+    f(100)  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 3
+    assert all(isinstance(node, nodes.Const) for node in inferred)
+    assert {node.value for node in inferred} == {1, 2, 3}
+
+
+def test_argument():
+    """Test function whose return value uses its arguments"""
+    node = builder.extract_node(
+        """
+    def f(x, y):
+        return x + y
+
+    f(1, 2)  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], nodes.Const)
+    assert inferred[0].value == 3
+
+
+def test_inner_call():
+    """Test function where return value is the result of a separate function call"""
+    node = builder.extract_node(
+        """
+    def f():
+        return g()
+
+    def g():
+        return 1
+
+    f()  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], nodes.Const)
+    assert inferred[0].value == 1
+
+
+def test_inner_call_with_const_argument():
+    """Test function where return value is the result of a separate function call,
+    with a constant value passed to the inner function.
+    """
+    node = builder.extract_node(
+        """
+    def f():
+        return g(1)
+
+    def g(y):
+        return y + 2
+
+    f()  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], nodes.Const)
+    assert inferred[0].value == 3
+
+
+def test_inner_call_with_dynamic_argument():
+    """Test function where return value is the result of a separate function call,
+    with a dynamic value passed to the inner function.
+
+    Currently, this is Uninferable.
+    """
+    node = builder.extract_node(
+        """
+    def f(x):
+        return g(x)
+
+    def g(y):
+        return y + 2
+
+    f(1)  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert inferred[0] is Uninferable
+
+
+def test_method_const_instance_attr():
+    """Test method where the return value is based on an instance attribute with a
+    constant value.
+    """
+    node = builder.extract_node(
+        """
+    class A:
+        def __init__(self):
+            self.x = 1
+
+        def get_x(self):
+            return self.x
+
+    A().get_x()  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], nodes.Const)
+    assert inferred[0].value == 1
+
+
+def test_method_const_instance_attr_multiple():
+    """Test method where the return value is based on an instance attribute with
+    multiple possible constant values, across different methods.
+    """
+    node = builder.extract_node(
+        """
+    class A:
+        def __init__(self, x):
+            if x:
+                self.x = 1
+            else:
+                self.x = 2
+
+        def set_x(self):
+            self.x = 3
+
+        def get_x(self):
+            return self.x
+
+    A().get_x()  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 3
+    assert all(isinstance(node, nodes.Const) for node in inferred)
+    assert {node.value for node in inferred} == {1, 2, 3}
+
+
+def test_method_const_instance_attr_same_method():
+    """Test method where the return value is based on an instance attribute with
+    multiple possible constant values, including in the method being called.
+
+    Note that even with a simple control flow where the assignment in the method body
+    is guaranteed to override any previous assignments, all possible constant values
+    are returned.
+    """
+    node = builder.extract_node(
+        """
+    class A:
+        def __init__(self, x):
+            if x:
+                self.x = 1
+            else:
+                self.x = 2
+
+        def set_x(self):
+            self.x = 3
+
+        def get_x(self):
+            self.x = 4
+            return self.x
+
+    A().get_x()  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 4
+    assert all(isinstance(node, nodes.Const) for node in inferred)
+    assert {node.value for node in inferred} == {1, 2, 3, 4}
+
+
+def test_method_dynamic_instance_attr_1():
+    """Test method where the return value is based on an instance attribute with
+    a dynamically-set value in a different method.
+
+    In this case, the return value is Uninferable.
+    """
+    node = builder.extract_node(
+        """
+    class A:
+        def __init__(self, x):
+            self.x = x
+
+        def get_x(self):
+            return self.x
+
+    A(1).get_x()  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert inferred[0] is Uninferable
+
+
+def test_method_dynamic_instance_attr_2():
+    """Test method where the return value is based on an instance attribute with
+    a dynamically-set value in the same method.
+    """
+    node = builder.extract_node(
+        """
+    class A:
+        # Note: no initializer, so the only assignment happens in get_x
+
+        def get_x(self, x):
+            self.x = x
+            return self.x
+
+    A().get_x(1)  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], nodes.Const)
+    assert inferred[0].value == 1
+
+
+def test_method_dynamic_instance_attr_3():
+    """Test method where the return value is based on an instance attribute with
+    a dynamically-set value in a different method.
+
+    This is currently Uninferable.
+    """
+    node = builder.extract_node(
+        """
+    class A:
+        def get_x(self, x):  # x is unused
+            return self.x
+
+        def set_x(self, x):
+            self.x = x
+
+    A().get_x(10)  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert inferred[0] is Uninferable  # not 10!
+
+
+def test_method_dynamic_instance_attr_4():
+    """Test method where the return value is based on an instance attribute with
+    a dynamically-set value in a different method, and is passed a constant value.
+
+    This is currently Uninferable.
+    """
+    node = builder.extract_node(
+        """
+    class A:
+        # Note: no initializer, so the only assignment happens in get_x
+
+        def get_x(self):
+            self.set_x(10)
+            return self.x
+
+        def set_x(self, x):
+            self.x = x
+
+    A().get_x()  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert inferred[0] is Uninferable
+
+
+def test_method_dynamic_instance_attr_5():
+    """Test method where the return value is based on an instance attribute with
+    a dynamically-set value in a different method, and is passed a constant value.
+
+    But, where the outer and inner functions have the same signature.
+
+    Inspired by https://github.com/PyCQA/pylint/issues/400
+
+    This is currently Uninferable.
+    """
+    node = builder.extract_node(
+        """
+    class A:
+        # Note: no initializer, so the only assignment happens in get_x
+
+        def get_x(self, x):
+            self.set_x(10)
+            return self.x
+
+        def set_x(self, x):
+            self.x = x
+
+    A().get_x(1)  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert inferred[0] is Uninferable
+
+
+def test_method_dynamic_instance_attr_6():
+    """Test method where the return value is based on an instance attribute with
+    a dynamically-set value in a different method, and is passed a dynamic value.
+
+    This is currently Uninferable.
+    """
+    node = builder.extract_node(
+        """
+    class A:
+        # Note: no initializer, so the only assignment happens in get_x
+
+        def get_x(self, x):
+            self.set_x(x + 1)
+            return self.x
+
+        def set_x(self, x):
+            self.x = x
+
+    A().get_x(1)  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert inferred[0] is Uninferable
+
+
+def test_dunder_getitem():
+    """Test for the special method __getitem__ (used by Instance.getitem).
+
+    This is currently Uninferable, until we can infer instance attribute values through
+    constructor calls.
+    """
+    node = builder.extract_node(
+        """
+    class A:
+        def __init__(self, x):
+            self.x = x
+
+        def __getitem__(self, i):
+            return self.x + i
+
+    A(1)[2]  #@
+    """
+    )
+
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert inferred[0] is Uninferable
+
+
+def test_instance_method():
+    """Tests for instance method, both bound and unbound."""
+    nodes_ = builder.extract_node(
+        """
+    class A:
+        def method(self, x):
+            return x
+
+    A().method(42)  #@
+
+    # In this case, the 1 argument is bound to self, which is ignored in the method
+    A.method(1, 42)  #@
+    """
+    )
+
+    for node in nodes_:
+        inferred = node.inferred()
+        assert len(inferred) == 1
+        assert isinstance(inferred[0], nodes.Const)
+        assert inferred[0].value == 42
+
+
+def test_class_method():
+    """Tests for class method calls, both instance and with the class."""
+    nodes_ = builder.extract_node(
+        """
+    class A:
+        @classmethod
+        def method(cls, x):
+            return x
+
+    A.method(42)  #@
+    A().method(42)  #@
+
+    """
+    )
+
+    for node in nodes_:
+        inferred = node.inferred()
+        assert len(inferred) == 1
+        assert isinstance(inferred[0], nodes.Const), node
+        assert inferred[0].value == 42
+
+
+def test_static_method():
+    """Tests for static method calls, both instance and with the class."""
+    nodes_ = builder.extract_node(
+        """
+    class A:
+        @staticmethod
+        def method(x):
+            return x
+
+    A.method(42)  #@
+    A().method(42)  #@
+    """
+    )
+
+    for node in nodes_:
+        inferred = node.inferred()
+        assert len(inferred) == 1
+        assert isinstance(inferred[0], nodes.Const), node
+        assert inferred[0].value == 42
+
+
+def test_instance_method_inherited():
+    """Tests for instance methods that are inherited from a superclass.
+
+    Based on https://github.com/PyCQA/astroid/issues/1008.
+    """
+    nodes_ = builder.extract_node(
+        """
+    class A:
+        def method(self):
+            return self
+
+    class B(A):
+        pass
+
+    A().method()  #@
+    A.method(A())  #@
+
+    B().method()  #@
+    B.method(B())  #@
+    A.method(B())  #@
+    """
+    )
+    expected = ["A", "A", "B", "B", "B"]
+    for node, expected in zip(nodes_, expected):
+        inferred = node.inferred()
+        assert len(inferred) == 1
+        assert isinstance(inferred[0], bases.Instance)
+        assert inferred[0].name == expected
+
+
+def test_class_method_inherited():
+    """Tests for class methods that are inherited from a superclass.
+
+    Based on https://github.com/PyCQA/astroid/issues/1008.
+    """
+    nodes_ = builder.extract_node(
+        """
+    class A:
+        @classmethod
+        def method(cls):
+            return cls
+
+    class B(A):
+        pass
+
+    A().method()  #@
+    A.method()  #@
+
+    B().method()  #@
+    B.method()  #@
+    """
+    )
+    expected = ["A", "A", "B", "B"]
+    for node, expected in zip(nodes_, expected):
+        inferred = node.inferred()
+        assert len(inferred) == 1
+        assert isinstance(inferred[0], nodes.ClassDef)
+        assert inferred[0].name == expected
+
+
+def test_chained_attribute_inherited():
+    """Tests for class methods that are inherited from a superclass.
+
+    Based on https://github.com/PyCQA/pylint/issues/4220.
+    """
+    node = builder.extract_node(
+        """
+    class A:
+        def f(self):
+            return 42
+
+
+    class B(A):
+        def __init__(self):
+            self.a = A()
+            result = self.a.f()
+
+        def f(self):
+            pass
+
+
+    B().a.f()  #@
+    """
+    )
+    inferred = node.inferred()
+    assert len(inferred) == 1
+    assert isinstance(inferred[0], nodes.Const)
+    assert inferred[0].value == 42