Repository: pylint-dev/astroid

Issue:
Raise an error if .root() is not a module
Originally reported by: **Claudiu Popa (BitBucket: [PCManticore](http://bitbucket.org/PCManticore), GitHub: @PCManticore)**

---

.root() should guarantee that what it returns is a module. Unfortunately, I found a couple of cases, which involved transforms, where .root() return classes instead of modules. We should raise an error in that case (and probably we should fix whatever was returning a class to return a module instead).

---
- Bitbucket: https://bitbucket.org/logilab/astroid/issue/120

Hints from issue comments:
@pylint-bot: _Original comment by_ **Claudiu Popa (BitBucket: [PCManticore](http://bitbucket.org/PCManticore), GitHub: @PCManticore)**:

---

Actually this can happen if the tree was spliced from another and it is in the course of being transformed.
@temyurchenko: Why is this completed? ".root" sometimes returns not a module. In particular, for some nodes made with `const_factory`@Pierre-Sassoulas: #2672 gave an example of root not being a module but Unknown instead.
```python
a=eval.__get__(1).__gt__

@a
class c: ...
```

Solution patch:
diff --git a/astroid/interpreter/objectmodel.py b/astroid/interpreter/objectmodel.py
index fd8c0c05..085bd2cc 100644
--- a/astroid/interpreter/objectmodel.py
+++ b/astroid/interpreter/objectmodel.py
@@ -228,17 +228,17 @@ class ModuleModel(ObjectModel):
     @property
     def attr___spec__(self):
         # No handling for now.
-        return node_classes.Unknown()
+        return node_classes.Unknown(parent=self._instance)
 
     @property
     def attr___loader__(self):
         # No handling for now.
-        return node_classes.Unknown()
+        return node_classes.Unknown(parent=self._instance)
 
     @property
     def attr___cached__(self):
         # No handling for now.
-        return node_classes.Unknown()
+        return node_classes.Unknown(parent=self._instance)
 
 
 class FunctionModel(ObjectModel):
@@ -462,7 +462,7 @@ class FunctionModel(ObjectModel):
     # These are here just for completion.
     @property
     def attr___ne__(self):
-        return node_classes.Unknown()
+        return node_classes.Unknown(parent=self._instance)
 
     attr___subclasshook__ = attr___ne__
     attr___str__ = attr___ne__
@@ -493,8 +493,8 @@ class ClassModel(ObjectModel):
         super().__init__()
 
     @property
-    def attr___annotations__(self) -> node_classes.Unkown:
-        return node_classes.Unknown()
+    def attr___annotations__(self) -> node_classes.Unknown:
+        return node_classes.Unknown(parent=self._instance)
 
     @property
     def attr___module__(self):
diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py
index ebfd5ffe..d3aef171 100644
--- a/astroid/nodes/node_classes.py
+++ b/astroid/nodes/node_classes.py
@@ -4963,9 +4963,9 @@ class Unknown(_base_nodes.AssignTypeNode):
 
     def __init__(
         self,
+        parent: NodeNG,
         lineno: None = None,
         col_offset: None = None,
-        parent: None = None,
         *,
         end_lineno: None = None,
         end_col_offset: None = None,
@@ -4986,6 +4986,9 @@ class Unknown(_base_nodes.AssignTypeNode):
         yield util.Uninferable
 
 
+UNATTACHED_UNKNOWN = Unknown(parent=SYNTHETIC_ROOT)
+
+
 class EvaluatedObject(NodeNG):
     """Contains an object that has already been inferred
 
diff --git a/astroid/nodes/node_ng.py b/astroid/nodes/node_ng.py
index d3be0472..dc8942b7 100644
--- a/astroid/nodes/node_ng.py
+++ b/astroid/nodes/node_ng.py
@@ -309,13 +309,13 @@ class NodeNG:
             raise ParentMissingError(target=self)
         return self.parent.scope()
 
-    def root(self) -> nodes.Module | nodes.Unknown:
+    def root(self) -> nodes.Module:
         """Return the root node of the syntax tree.
 
         :returns: The root node.
         """
         if not (parent := self.parent):
-            assert isinstance(self, (nodes.Module, nodes.Unknown))
+            assert isinstance(self, nodes.Module)
             return self
 
         while parent.parent:
diff --git a/astroid/protocols.py b/astroid/protocols.py
index 8a837b86..7cc50810 100644
--- a/astroid/protocols.py
+++ b/astroid/protocols.py
@@ -162,13 +162,13 @@ def _filter_uninferable_nodes(
 ) -> Iterator[SuccessfulInferenceResult]:
     for elt in elts:
         if isinstance(elt, util.UninferableBase):
-            yield nodes.Unknown()
+            yield node_classes.UNATTACHED_UNKNOWN
         else:
             for inferred in elt.infer(context):
                 if not isinstance(inferred, util.UninferableBase):
                     yield inferred
                 else:
-                    yield nodes.Unknown()
+                    yield node_classes.UNATTACHED_UNKNOWN
 
 
 @decorators.yes_if_nothing_inferred


Test patch:
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 170176f9..12168cf6 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -11,6 +11,7 @@ from astroid import builder, helpers, manager, nodes, raw_building, util
 from astroid.builder import AstroidBuilder
 from astroid.const import IS_PYPY
 from astroid.exceptions import _NonDeducibleTypeHierarchy
+from astroid.nodes.node_classes import UNATTACHED_UNKNOWN
 from astroid.nodes.scoped_nodes import ClassDef
 
 
@@ -269,7 +270,7 @@ def test_uninferable_for_safe_infer() -> None:
 
 def test_safe_infer_shim() -> None:
     with pytest.warns(DeprecationWarning) as records:
-        helpers.safe_infer(nodes.Unknown())
+        helpers.safe_infer(UNATTACHED_UNKNOWN)
 
     assert (
         "Import safe_infer from astroid.util; this shim in astroid.helpers will be removed."
diff --git a/tests/test_nodes.py b/tests/test_nodes.py
index ffa51158..d6e3152d 100644
--- a/tests/test_nodes.py
+++ b/tests/test_nodes.py
@@ -37,6 +37,7 @@ from astroid.exceptions import (
     StatementMissing,
 )
 from astroid.nodes.node_classes import (
+    UNATTACHED_UNKNOWN,
     AssignAttr,
     AssignName,
     Attribute,
@@ -281,8 +282,10 @@ everything = f""" " \' \r \t \\ {{ }} {'x' + x!r:a} {["'"]!s:{a}}"""
 
     @staticmethod
     def test_as_string_unknown() -> None:
-        assert nodes.Unknown().as_string() == "Unknown.Unknown()"
-        assert nodes.Unknown(lineno=1, col_offset=0).as_string() == "Unknown.Unknown()"
+        unknown1 = nodes.Unknown(parent=SYNTHETIC_ROOT)
+        unknown2 = nodes.Unknown(lineno=1, col_offset=0, parent=SYNTHETIC_ROOT)
+        assert unknown1.as_string() == "Unknown.Unknown()"
+        assert unknown2.as_string() == "Unknown.Unknown()"
 
     @staticmethod
     @pytest.mark.skipif(
@@ -1231,9 +1234,9 @@ class ContextTest(unittest.TestCase):
 
 def test_unknown() -> None:
     """Test Unknown node."""
-    assert isinstance(next(nodes.Unknown().infer()), type(util.Uninferable))
-    assert isinstance(nodes.Unknown().name, str)
-    assert isinstance(nodes.Unknown().qname(), str)
+    assert isinstance(next(UNATTACHED_UNKNOWN.infer()), type(util.Uninferable))
+    assert isinstance(UNATTACHED_UNKNOWN.name, str)
+    assert isinstance(UNATTACHED_UNKNOWN.qname(), str)
 
 
 def test_type_comments_with() -> None:
@@ -1963,7 +1966,7 @@ def test_str_repr_no_warnings(node):
             "NodeNG" in param_type.annotation
             or "SuccessfulInferenceResult" in param_type.annotation
         ):
-            args[name] = nodes.Unknown()
+            args[name] = UNATTACHED_UNKNOWN
         elif "str" in param_type.annotation:
             args[name] = ""
         else:
diff --git a/tests/test_regrtest.py b/tests/test_regrtest.py
index 9b347177..861f4e26 100644
--- a/tests/test_regrtest.py
+++ b/tests/test_regrtest.py
@@ -502,7 +502,7 @@ def test_regression_missing_callcontext() -> None:
 
 def test_regression_root_is_not_a_module() -> None:
     """Regression test for #2672."""
-    node: nodes.Attribute = _extract_single_node(
+    node: nodes.ClassDef = _extract_single_node(
         textwrap.dedent(
             """
         a=eval.__get__(1).__gt__
@@ -515,6 +515,14 @@ def test_regression_root_is_not_a_module() -> None:
     assert node.name == "c"
 
 
+@pytest.mark.xfail(reason="Not fixed yet")
+def test_regression_eval_get_of_arg() -> None:
+    """Regression test for #2743"""
+    node = _extract_single_node("eval.__get__(1)")
+    with pytest.raises(InferenceError):
+        next(node.infer())
+
+
 def test_regression_no_crash_during_build() -> None:
     node: nodes.Attribute = extract_node("__()")
     assert node.args == []