Repository: python/mypy
Pull request: New analyser: fix crashes when plugins encounter a placeholder node
Fixes https://github.com/python/mypy/issues/7098

Problem statement:
New analyser: Crash in attrs plugin on bad or deferred name
Both of these crash with the new analyser:
```
[case testTypeInAttrBad]
import attr

@attr.s
class C:
    total = attr.ib(type=Bad)

[builtins fixtures/bool.pyi]
```
and
```
[case testTypeInAttrBad2]
import attr

@attr.s
class C:
    total = attr.ib(default=func())

def func(): ...
[builtins fixtures/bool.pyi]
```
Both crash with an assertion error because plugin doesn't expect a placeholder in class symbol table. I didn't check dataclasses, but likely there is a similar issue.

Hints from issue discussion:

Commit message: Fix fixture

Patch to review:
diff --git a/mypy/plugins/attrs.py b/mypy/plugins/attrs.py
index 78afc422add6..f1866973e14c 100644
--- a/mypy/plugins/attrs.py
+++ b/mypy/plugins/attrs.py
@@ -270,7 +270,9 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext',
         # instance level assignments.
         if attribute.name in ctx.cls.info.names:
             node = ctx.cls.info.names[attribute.name].node
-            assert isinstance(node, Var)
+            if not isinstance(node, Var):
+                # This node may be not ready yet (i.e. a PlaceholderNode).
+                continue
             node.is_initialized_in_class = False
 
     # Traverse the MRO and collect attributes from the parents.
diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py
index dbe3695bf260..960111a4a22e 100644
--- a/mypy/plugins/dataclasses.py
+++ b/mypy/plugins/dataclasses.py
@@ -214,7 +214,9 @@ def collect_attributes(self) -> List[DataclassAttribute]:
                 continue
 
             node = cls.info.names[lhs.name].node
-            assert isinstance(node, Var)
+            if not isinstance(node, Var):
+                # This node may be not ready yet (i.e. a PlaceholderNode).
+                continue
 
             # x: ClassVar[int] is ignored by dataclasses.
             if node.is_classvar:
diff --git a/test-data/unit/check-attr.test b/test-data/unit/check-attr.test
index 6202b4e6aed4..d90447875e25 100644
--- a/test-data/unit/check-attr.test
+++ b/test-data/unit/check-attr.test
@@ -1107,3 +1107,47 @@ C(43)
 
 class Yes: ...
 [builtins fixtures/exception.pyi]
+
+[case testTypeInAttrBad]
+import attr
+
+@attr.s
+class C:
+    total = attr.ib(type=Bad)  # E: Name 'Bad' is not defined
+[builtins fixtures/bool.pyi]
+
+[case testTypeInAttrBad2]
+import attr
+
+@attr.s
+class C:
+    total = attr.ib(type=Forward)
+
+reveal_type(C.total)  # N: Revealed type is '__main__.Forward'
+C('no')  # E: Argument 1 to "C" has incompatible type "str"; expected "Forward"
+class Forward: ...
+[builtins fixtures/bool.pyi]
+
+[case testTypeInAttrBad3]
+import attr
+
+@attr.s
+class C:
+    total = attr.ib(default=func())
+
+def func() -> int: ...
+
+C()
+C(1)
+C(1, 2)  # E: Too many arguments for "C"
+[builtins fixtures/bool.pyi]
+
+[case testTypeInAttrBad4]
+import attr
+
+@attr.s(frozen=True)
+class C:
+    total = attr.ib(type=Bad)  # E: Name 'Bad' is not defined
+
+C(0).total = 1  # E: Property "total" defined in "C" is read-only
+[builtins fixtures/bool.pyi]
diff --git a/test-data/unit/check-dataclasses.test b/test-data/unit/check-dataclasses.test
index cac18b239d62..5150c257b2fd 100644
--- a/test-data/unit/check-dataclasses.test
+++ b/test-data/unit/check-dataclasses.test
@@ -642,3 +642,26 @@ a < b
 
 class Yes: ...
 [builtins fixtures/list.pyi]
+
+[case testDataclassFieldDeferred]
+from dataclasses import field, dataclass
+
+@dataclass
+class C:
+    x: int = field(default=func())
+
+def func() -> int: ...
+C('no')  # E: Argument 1 to "C" has incompatible type "str"; expected "int"
+[builtins fixtures/bool.pyi]
+
+[case testDataclassFieldDeferredFrozen]
+from dataclasses import field, dataclass
+
+@dataclass(frozen=True)
+class C:
+    x: int = field(default=func())
+
+def func() -> int: ...
+c: C
+c.x = 1  # E: Property "x" defined in "C" is read-only
+[builtins fixtures/bool.pyi]


Code review comments:
diff_hunk: @@ -270,7 +270,9 @@ def _analyze_class(ctx: 'mypy.plugin.ClassDefContext',
         # instance level assignments.
         if attribute.name in ctx.cls.info.names:
             node = ctx.cls.info.names[attribute.name].node
-            assert isinstance(node, Var)
+            if not isinstance(node, Var):
+                # This node may be not ready yet (i.e. a PlaceholderNode).
path: mypy/plugins/attrs.py
text: @user1:
Should we instead check for `PlaceholderNode`? If there's something unexpected there, weird things might happen.