Repository: python/mypy

Issue:
Crash when class defined in method used for instance variable's type
While thinking about https://github.com/python/mypy/issues/2535 I realized there's a similar case not involving NamedTuple (nor TypedDict):

- main.py:
```python
import crash
blah  # anything that triggers an error
```

- crash.py:
```python
class C:
    def f(self) -> None:
        class A:
            pass
        self.a = A()
```
This triggers a very similar crash in fixup.py when running `mypy -i main.py` **TWICE** (from a cold cache):
```
$ rm -rf .mypy_cache/
$ mypy -i main.py 
main.py:1: error: Name 'blah' is not defined
$ mypy -i main.py 
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/usr/local/lib/python3.6/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/Users/guido/src/mypy/mypy/__main__.py", line 5, in <module>
    main(None)
  File "/Users/guido/src/mypy/mypy/main.py", line 41, in main
    res = type_check_only(sources, bin_dir, options)
  File "/Users/guido/src/mypy/mypy/main.py", line 86, in type_check_only
    options=options)
  File "/Users/guido/src/mypy/mypy/build.py", line 183, in build
    dispatch(sources, manager)
  File "/Users/guido/src/mypy/mypy/build.py", line 1524, in dispatch
    process_graph(graph, manager)
  File "/Users/guido/src/mypy/mypy/build.py", line 1753, in process_graph
    process_fresh_scc(graph, prev_scc)
  File "/Users/guido/src/mypy/mypy/build.py", line 1822, in process_fresh_scc
    graph[id].fix_cross_refs()
  File "/Users/guido/src/mypy/mypy/build.py", line 1307, in fix_cross_refs
    fixup_module_pass_one(self.tree, self.manager.modules)
  File "/Users/guido/src/mypy/mypy/fixup.py", line 21, in fixup_module_pass_one
    node_fixer.visit_symbol_table(tree.names)
  File "/Users/guido/src/mypy/mypy/fixup.py", line 86, in visit_symbol_table
    self.visit_type_info(value.node)
  File "/Users/guido/src/mypy/mypy/fixup.py", line 55, in visit_type_info
    self.visit_symbol_table(info.names)
  File "/Users/guido/src/mypy/mypy/fixup.py", line 88, in visit_symbol_table
    value.node.accept(self)
  File "/Users/guido/src/mypy/mypy/nodes.py", line 658, in accept
    return visitor.visit_var(self)
  File "/Users/guido/src/mypy/mypy/fixup.py", line 131, in visit_var
    v.type.accept(self.type_fixer)
  File "/Users/guido/src/mypy/mypy/types.py", line 433, in accept
    return visitor.visit_instance(self)
  File "/Users/guido/src/mypy/mypy/fixup.py", line 144, in visit_instance
    node = lookup_qualified(self.modules, type_ref)
  File "/Users/guido/src/mypy/mypy/fixup.py", line 234, in lookup_qualified
    stnode = lookup_qualified_stnode(modules, name)
  File "/Users/guido/src/mypy/mypy/fixup.py", line 245, in lookup_qualified_stnode
    assert '.' in head, "Cannot find %s" % (name,)
AssertionError: Cannot find A
```


Discussion:

Resolving patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -847,7 +847,15 @@ def setup_class_def_analysis(self, defn: ClassDef) -> None:
             kind = MDEF
             if self.is_func_scope():
                 kind = LDEF
-            self.add_symbol(defn.name, SymbolTableNode(kind, defn.info), defn)
+            node = SymbolTableNode(kind, defn.info)
+            self.add_symbol(defn.name, node, defn)
+            if kind == LDEF:
+                # We need to preserve local classes, let's store them
+                # in globals under mangled unique names
+                local_name = defn.info._fullname + '@' + str(defn.line)
+                defn.info._fullname = self.cur_mod_id + '.' + local_name
+                defn.fullname = defn.info._fullname
+                self.globals[local_name] = node
 
     def analyze_base_classes(self, defn: ClassDef) -> None:
         """Analyze and set up base classes.


Test patch:
diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test
--- a/test-data/unit/check-incremental.test
+++ b/test-data/unit/check-incremental.test
@@ -1768,6 +1768,53 @@ main:2: error: Revealed type is 'TypedDict(x=builtins.int, _fallback=typing.Mapp
 main:3: error: Revealed type is 'TypedDict(x=builtins.int, _fallback=ntcrash.C.A@4)'
 main:4: error: Revealed type is 'def () -> ntcrash.C.A@4'
 
+[case testIncrementalInnerClassAttrInMethod]
+import crash
+nonexisting
+[file crash.py]
+class C:
+    def f(self) -> None:
+        class A:
+            pass
+        self.a = A()
+[out1]
+main:2: error: Name 'nonexisting' is not defined
+[out2]
+main:2: error: Name 'nonexisting' is not defined
+
+[case testIncrementalInnerClassAttrInMethodReveal]
+import crash
+reveal_type(crash.C().a)
+reveal_type(crash.D().a)
+[file crash.py]
+from typing import TypeVar, Generic
+T = TypeVar('T')
+class C:
+    def f(self) -> None:
+        class A:
+            pass
+        self.a = A()
+reveal_type(C().a)
+class D:
+    def f(self) -> None:
+        class A:
+            def g(self) -> None:
+                class B(Generic[T]):
+                    pass
+                self.b = B[int]()
+        self.a = A().b
+reveal_type(D().a)
+[out1]
+tmp/crash.py:8: error: Revealed type is 'crash.A@5'
+tmp/crash.py:17: error: Revealed type is 'crash.B@13[builtins.int*]'
+main:2: error: Revealed type is 'crash.A@5'
+main:3: error: Revealed type is 'crash.B@13[builtins.int*]'
+[out2]
+tmp/crash.py:8: error: Revealed type is 'crash.A@5'
+tmp/crash.py:17: error: Revealed type is 'crash.B@13[builtins.int*]'
+main:2: error: Revealed type is 'crash.A@5'
+main:3: error: Revealed type is 'crash.B@13[builtins.int*]'
+
 [case testGenericMethodRestoreMetaLevel]
 from typing import Dict
 
diff --git a/test-data/unit/semanal-classes.test b/test-data/unit/semanal-classes.test
--- a/test-data/unit/semanal-classes.test
+++ b/test-data/unit/semanal-classes.test
@@ -315,7 +315,7 @@ MypyFile:1(
         A
         PassStmt:2())
       ExpressionStmt:3(
-        NameExpr(A [l])))))
+        NameExpr(A [__main__.A@2])))))
 
 [case testReferenceToClassWithinClass]
 class A:
@@ -364,7 +364,7 @@ MypyFile:1(
       AssignmentStmt:3(
         NameExpr(x [l])
         NameExpr(None [builtins.None])
-        A))))
+        __main__.A@2))))
 
 [case testAccessToLocalInOuterScopeWithinNestedClass]
 def f(x):