Repository: python/mypy

Issue:
“AssertionError: Cannot find component” for local NamedTuple inside class method
**Crash Report**

This is a variant of #7281 that still crashes incremental checking on 0.910 and `master` (9f409c3d89c7fa65eff8cf67883473487e6effd4), both of which include #10432, which was supposed to fix #7281.

The difference in this case is that the local `NamedTuple` is defined inside a class method rather than a global function.

**To Reproduce**

```sh
#!/bin/sh -eux

rm -rf .mypy_cache

echo 'from typing import NamedTuple' >breaking.py
echo 'class C:' >>breaking.py
echo '  def test(self):' >>breaking.py
echo '    class NT(NamedTuple):' >>breaking.py
echo '      pass' >>breaking.py

echo 'import breaking' >script.py
mypy script.py

echo '' >script.py
mypy script.py

echo 'import breaking' >script.py
mypy script.py
```

**Traceback**

```pytb
Traceback (most recent call last):
  File "/home/anders/python/mypy/.direnv/python-3.9.5/bin/mypy", line 33, in <module>
    sys.exit(load_entry_point('mypy', 'console_scripts', 'mypy')())
  File "/home/anders/python/mypy/mypy/__main__.py", line 11, in console_entry
    main(None, sys.stdout, sys.stderr)
  File "/home/anders/python/mypy/mypy/main.py", line 87, in main
    res, messages, blockers = run_build(sources, options, fscache, t0, stdout, stderr)
  File "/home/anders/python/mypy/mypy/main.py", line 165, in run_build
    res = build.build(sources, options, None, flush_errors, fscache, stdout, stderr)
  File "/home/anders/python/mypy/mypy/build.py", line 179, in build
    result = _build(
  File "/home/anders/python/mypy/mypy/build.py", line 254, in _build
    graph = dispatch(sources, manager, stdout)
  File "/home/anders/python/mypy/mypy/build.py", line 2698, in dispatch
    process_graph(graph, manager)
  File "/home/anders/python/mypy/mypy/build.py", line 3015, in process_graph
    process_fresh_modules(graph, prev_scc, manager)
  File "/home/anders/python/mypy/mypy/build.py", line 3093, in process_fresh_modules
    graph[id].fix_cross_refs()
  File "/home/anders/python/mypy/mypy/build.py", line 1992, in fix_cross_refs
    fixup_module(self.tree, self.manager.modules,
  File "/home/anders/python/mypy/mypy/fixup.py", line 26, in fixup_module
    node_fixer.visit_symbol_table(tree.names, tree.fullname)
  File "/home/anders/python/mypy/mypy/fixup.py", line 77, in visit_symbol_table
    stnode = lookup_qualified_stnode(self.modules, cross_ref,
  File "/home/anders/python/mypy/mypy/fixup.py", line 306, in lookup_qualified_stnode
    return lookup_fully_qualified(name, modules, raise_on_missing=not allow_missing)
  File "/home/anders/python/mypy/mypy/lookup.py", line 47, in lookup_fully_qualified
    assert key in names, "Cannot find component %r for %r" % (key, name)
AssertionError: Cannot find component 'NT@4' for 'breaking.C.NT@4'
```

**Your Environment**

- Mypy version used: 0.910, 0.920+dev.9f409c3d89c7fa65eff8cf67883473487e6effd4
- Mypy command-line flags: none
- Mypy configuration options from `mypy.ini` (and other config files): none
- Python version used: 3.9.5
- Operating system and version: NixOS 21.11



Discussion:
Similar traceback in #11121

Resolving patch:
diff --git a/mypy/semanal.py b/mypy/semanal.py
--- a/mypy/semanal.py
+++ b/mypy/semanal.py
@@ -1220,7 +1220,7 @@ def analyze_namedtuple_classdef(self, defn: ClassDef) -> bool:
             is_named_tuple, info = True, defn.info  # type: bool, Optional[TypeInfo]
         else:
             is_named_tuple, info = self.named_tuple_analyzer.analyze_namedtuple_classdef(
-                defn, self.is_stub_file)
+                defn, self.is_stub_file, self.is_func_scope())
         if is_named_tuple:
             if info is None:
                 self.mark_incomplete(defn.name, defn)
@@ -1462,7 +1462,10 @@ def prepare_class_def(self, defn: ClassDef, info: Optional[TypeInfo] = None) ->
                 info._fullname = self.qualified_name(defn.name)
             else:
                 info._fullname = info.name
-        self.add_symbol(defn.name, defn.info, defn)
+        local_name = defn.name
+        if '@' in local_name:
+            local_name = local_name.split('@')[0]
+        self.add_symbol(local_name, defn.info, defn)
         if self.is_nested_within_func_scope():
             # We need to preserve local classes, let's store them
             # in globals under mangled unique names
@@ -1471,17 +1474,17 @@ def prepare_class_def(self, defn: ClassDef, info: Optional[TypeInfo] = None) ->
             #       incremental mode and we should avoid it. In general, this logic is too
             #       ad-hoc and needs to be removed/refactored.
             if '@' not in defn.info._fullname:
-                local_name = defn.info.name + '@' + str(defn.line)
-                if defn.info.is_named_tuple:
-                    # Module is already correctly set in _fullname for named tuples.
-                    defn.info._fullname += '@' + str(defn.line)
-                else:
-                    defn.info._fullname = self.cur_mod_id + '.' + local_name
+                global_name = defn.info.name + '@' + str(defn.line)
+                defn.info._fullname = self.cur_mod_id + '.' + global_name
             else:
                 # Preserve name from previous fine-grained incremental run.
-                local_name = defn.info.name
+                global_name = defn.info.name
             defn.fullname = defn.info._fullname
-            self.globals[local_name] = SymbolTableNode(GDEF, defn.info)
+            if defn.info.is_named_tuple:
+                # Named tuple nested within a class is stored in the class symbol table.
+                self.add_symbol_skip_local(global_name, defn.info)
+            else:
+                self.globals[global_name] = SymbolTableNode(GDEF, defn.info)
 
     def make_empty_type_info(self, defn: ClassDef) -> TypeInfo:
         if (self.is_module_scope()
diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py
--- a/mypy/semanal_namedtuple.py
+++ b/mypy/semanal_namedtuple.py
@@ -53,7 +53,8 @@ def __init__(self, options: Options, api: SemanticAnalyzerInterface) -> None:
         self.options = options
         self.api = api
 
-    def analyze_namedtuple_classdef(self, defn: ClassDef, is_stub_file: bool
+    def analyze_namedtuple_classdef(self, defn: ClassDef, is_stub_file: bool,
+                                    is_func_scope: bool
                                     ) -> Tuple[bool, Optional[TypeInfo]]:
         """Analyze if given class definition can be a named tuple definition.
 
@@ -70,6 +71,8 @@ def analyze_namedtuple_classdef(self, defn: ClassDef, is_stub_file: bool
                         # This is a valid named tuple, but some types are incomplete.
                         return True, None
                     items, types, default_items = result
+                    if is_func_scope and '@' not in defn.name:
+                        defn.name += '@' + str(defn.line)
                     info = self.build_namedtuple_typeinfo(
                         defn.name, items, types, default_items, defn.line)
                     defn.info = info


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
@@ -5658,3 +5658,56 @@ class D(C):
 [out]
 [out2]
 tmp/a.py:9: error: Trying to assign name "z" that is not in "__slots__" of type "a.D"
+
+[case testIncrementalWithDifferentKindsOfNestedTypesWithinMethod]
+# flags: --python-version 3.7
+
+import a
+
+[file a.py]
+import b
+
+[file a.py.2]
+import b
+b.xyz
+
+[file b.py]
+from typing import NamedTuple, NewType
+from typing_extensions import TypedDict, TypeAlias
+from enum import Enum
+from dataclasses import dataclass
+
+class C:
+    def f(self) -> None:
+        class C:
+            c: int
+        class NT1(NamedTuple):
+            c: int
+        NT2 = NamedTuple("NT2", [("c", int)])
+        class NT3(NT1):
+            pass
+        class TD(TypedDict):
+            c: int
+        TD2 = TypedDict("TD2", {"c": int})
+        class E(Enum):
+            X = 1
+        @dataclass
+        class DC:
+            c: int
+        Alias: TypeAlias = NT1
+        N = NewType("N", NT1)
+
+        c: C = C()
+        nt1: NT1 = NT1(c=1)
+        nt2: NT2 = NT2(c=1)
+        nt3: NT3 = NT3(c=1)
+        td: TD = TD(c=1)
+        td2: TD2 = TD2(c=1)
+        e: E = E.X
+        dc: DC = DC(c=1)
+        al: Alias = Alias(c=1)
+        n: N = N(NT1(c=1))
+
+[builtins fixtures/dict.pyi]
+[out2]
+tmp/a.py:2: error: "object" has no attribute "xyz"