Repository: pylint-dev/astroid

Issue:
Delayed attribute assignment to object() may cause incorrect inference of instance attributes
@cdce8p: `aiohttp` and `VLCTelnet` turned out to be red herrings. This case fails on current stable versions:

```python
class Example:
    def prev(self):
        pass
    def next(self):
        pass
    def other(self):
        pass


ex = Example()
ex.other()  # no warning
ex.prev()   # no warning
ex.next()   # no warning

import typing

ex.other()  # no warning
ex.prev()   # false-positive: not-callable
ex.next()   # false-positive: not-callable
```

_Originally posted by @nelfin in https://github.com/PyCQA/astroid/issues/927#issuecomment-818626368_

I've bisected this down to 78d5537. Pylint 2.3.1 passes this case with 20a7ae5 and fails with 78d5537


Discussion / hints:
Minimal case:

```python
class Example:
    def func(self):
        pass


whatthe = object()
whatthe.func = None

ex = Example()
ex.func()   # false-positive: not-callable
```
Not caused by 78d5537, just revealed by it. `typing` imported `collections`, `collections.OrderedDict` had an ambiguously inferred case that was previously broken by failure with positional-only arguments which was fixed in 78d5537.



Fix (patch):
diff --git a/ChangeLog b/ChangeLog
index b4af947daa..12fae5ceab 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -6,6 +6,15 @@ What's New in astroid 2.6.0?
 ============================
 Release Date: TBA
 
+* Do not set instance attributes on builtin object()
+
+ Closes #945
+ Closes PyCQA/pylint#4232
+ Closes PyCQA/pylint#4221
+ Closes PyCQA/pylint#3970
+ Closes PyCQA/pylint#3595
+
+
 What's New in astroid 2.5.6?
 ============================
 Release Date: 2021-04-25
diff --git a/astroid/builder.py b/astroid/builder.py
index 6a7f79ced0..4a066ee836 100644
--- a/astroid/builder.py
+++ b/astroid/builder.py
@@ -67,7 +67,7 @@ def _can_assign_attr(node, attrname):
     else:
         if slots and attrname not in {slot.value for slot in slots}:
             return False
-    return True
+    return node.qname() != "builtins.object"
 
 
 class AstroidBuilder(raw_building.InspectBuilder):


Test changes:
diff --git a/tests/unittest_builder.py b/tests/unittest_builder.py
index a48d341999..ce9abf9af0 100644
--- a/tests/unittest_builder.py
+++ b/tests/unittest_builder.py
@@ -28,7 +28,7 @@
 
 import pytest
 
-from astroid import builder, exceptions, manager, nodes, test_utils, util
+from astroid import Instance, builder, exceptions, manager, nodes, test_utils, util
 
 from . import resources
 
@@ -476,6 +476,53 @@ def A_assign_type(self):
         self.assertIn("assign_type", lclass.locals)
         self.assertIn("type", lclass.locals)
 
+    def test_infer_can_assign_regular_object(self):
+        mod = builder.parse(
+            """
+            class A:
+                pass
+            a = A()
+            a.value = "is set"
+            a.other = "is set"
+        """
+        )
+        obj = list(mod.igetattr("a"))
+        self.assertEqual(len(obj), 1)
+        obj = obj[0]
+        self.assertIsInstance(obj, Instance)
+        self.assertIn("value", obj.instance_attrs)
+        self.assertIn("other", obj.instance_attrs)
+
+    def test_infer_can_assign_has_slots(self):
+        mod = builder.parse(
+            """
+            class A:
+                __slots__ = ('value',)
+            a = A()
+            a.value = "is set"
+            a.other = "not set"
+        """
+        )
+        obj = list(mod.igetattr("a"))
+        self.assertEqual(len(obj), 1)
+        obj = obj[0]
+        self.assertIsInstance(obj, Instance)
+        self.assertIn("value", obj.instance_attrs)
+        self.assertNotIn("other", obj.instance_attrs)
+
+    def test_infer_can_assign_no_classdict(self):
+        mod = builder.parse(
+            """
+            a = object()
+            a.value = "not set"
+        """
+        )
+        obj = list(mod.igetattr("a"))
+        self.assertEqual(len(obj), 1)
+        obj = obj[0]
+        self.assertIsInstance(obj, Instance)
+        self.assertNotIn("value", obj.instance_attrs)
+
     def test_augassign_attr(self):
         builder.parse(
             """
diff --git a/tests/unittest_scoped_nodes.py b/tests/unittest_scoped_nodes.py
index dd6102c6ff..9303f1c7eb 100644
--- a/tests/unittest_scoped_nodes.py
+++ b/tests/unittest_scoped_nodes.py
@@ -1430,7 +1430,9 @@ class A:
             pass
         class B:
             pass
-        scope = object()
+        class Scope:
+            pass
+        scope = Scope()
         scope.A = A
         scope.B = B
         class C(scope.A, scope.B):