Repository: python/mypy

Issue:
Incremental crash with double re-exported alias used in function annotation
Here is a (relatively) minimal repro:
```
[case testOidCrash]
import m

[file m.py]
import f
[file m.py.3]
import f
# modify

[file f.py]
import c
def foo(arg: c.Oid) -> None: pass

[file c.py]
from types import Oid

[file types.py]
import pb1
Oid = pb1.Oid
[file types.py.2]
import pb1, pb2
Oid = pb2.Oid

[file pb1.py]
class Oid: ...
[file pb2.py.2]
class Oid: ...
[file pb1.py.2]

[out]
[out2]
[out3]
```
This crashes with
```
/home/ivan/src/mypy/mypy/test/testcheck.py:81: in run_case
    self.run_case_once(testcase, ops, step)
/home/ivan/src/mypy/mypy/test/testcheck.py:147: in run_case_once
    res = build.build(sources=sources, options=options, alt_lib_path=test_temp_dir)
/home/ivan/src/mypy/mypy/build.py:193: in build
    result = _build(
/home/ivan/src/mypy/mypy/build.py:276: in _build
    graph = dispatch(sources, manager, stdout)
/home/ivan/src/mypy/mypy/build.py:2891: in dispatch
    process_graph(graph, manager)
/home/ivan/src/mypy/mypy/build.py:3268: in process_graph
    process_fresh_modules(graph, prev_scc, manager)
/home/ivan/src/mypy/mypy/build.py:3349: in process_fresh_modules
    graph[id].fix_cross_refs()
/home/ivan/src/mypy/mypy/build.py:2112: in fix_cross_refs
    fixup_module(self.tree, self.manager.modules, self.options.use_fine_grained_cache)
/home/ivan/src/mypy/mypy/fixup.py:53: in fixup_module
    node_fixer.visit_symbol_table(tree.names, tree.fullname)
/home/ivan/src/mypy/mypy/fixup.py:127: in visit_symbol_table
    value.node.accept(self)
/home/ivan/src/mypy/mypy/nodes.py:822: in accept
    return visitor.visit_func_def(self)
/home/ivan/src/mypy/mypy/fixup.py:135: in visit_func_def
    func.type.accept(self.type_fixer)
/home/ivan/src/mypy/mypy/types.py:1792: in accept
    return visitor.visit_callable_type(self)
/home/ivan/src/mypy/mypy/fixup.py:229: in visit_callable_type
    argt.accept(self)
/home/ivan/src/mypy/mypy/types.py:1283: in accept
    return visitor.visit_instance(self)
/home/ivan/src/mypy/mypy/fixup.py:196: in visit_instance
    inst.type = lookup_fully_qualified_typeinfo(
/home/ivan/src/mypy/mypy/fixup.py:331: in lookup_fully_qualified_typeinfo
    stnode = lookup_fully_qualified(name, modules, raise_on_missing=not allow_missing)
/home/ivan/src/mypy/mypy/lookup.py:49: in lookup_fully_qualified
    assert key in names, f"Cannot find component {key!r} for {name!r}"
AssertionError: Cannot find component 'Oid' for 'pb1.Oid'
```
The problem is that `TypeIndirectionVisitor` uses `type_map` for a given module to find indirect dependencies. This is however problematic in various cases, for example types used in a function definition may never appear as a type of some expression (type map of `f.py` in this case is just empty). I am not sure what is the best solution (for example fine-grained mode doesn't have this problem as it uses a proper node visitor). Maybe we can just add all types encountered in symbol tables?

cc @JukkaL @Michael0x2a 


Discussion:

Resolving patch:
diff --git a/mypy/build.py b/mypy/build.py
--- a/mypy/build.py
+++ b/mypy/build.py
@@ -48,7 +48,7 @@
 from mypy.errors import CompileError, ErrorInfo, Errors, report_internal_error
 from mypy.indirection import TypeIndirectionVisitor
 from mypy.messages import MessageBuilder
-from mypy.nodes import Import, ImportAll, ImportBase, ImportFrom, MypyFile, SymbolTable
+from mypy.nodes import Import, ImportAll, ImportBase, ImportFrom, MypyFile, SymbolTable, TypeInfo
 from mypy.partially_defined import PartiallyDefinedVariableVisitor
 from mypy.semanal import SemanticAnalyzer
 from mypy.semanal_pass1 import SemanticAnalyzerPreAnalysis
@@ -2363,7 +2363,24 @@ def finish_passes(self) -> None:
 
             # We should always patch indirect dependencies, even in full (non-incremental) builds,
             # because the cache still may be written, and it must be correct.
-            self._patch_indirect_dependencies(self.type_checker().module_refs, self.type_map())
+            # TODO: find a more robust way to traverse *all* relevant types?
+            expr_types = set(self.type_map().values())
+            symbol_types = set()
+            for _, sym, _ in self.tree.local_definitions():
+                if sym.type is not None:
+                    symbol_types.add(sym.type)
+                if isinstance(sym.node, TypeInfo):
+                    # TypeInfo symbols have some extra relevant types.
+                    symbol_types.update(sym.node.bases)
+                    if sym.node.metaclass_type:
+                        symbol_types.add(sym.node.metaclass_type)
+                    if sym.node.typeddict_type:
+                        symbol_types.add(sym.node.typeddict_type)
+                    if sym.node.tuple_type:
+                        symbol_types.add(sym.node.tuple_type)
+            self._patch_indirect_dependencies(
+                self.type_checker().module_refs, expr_types | symbol_types
+            )
 
             if self.options.dump_inference_stats:
                 dump_type_stats(
@@ -2386,10 +2403,7 @@ def free_state(self) -> None:
             self._type_checker.reset()
             self._type_checker = None
 
-    def _patch_indirect_dependencies(
-        self, module_refs: set[str], type_map: dict[Expression, Type]
-    ) -> None:
-        types = set(type_map.values())
+    def _patch_indirect_dependencies(self, module_refs: set[str], types: set[Type]) -> None:
         assert None not in types
         valid = self.valid_references()
 


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
@@ -6081,3 +6081,207 @@ class Base:
 [out]
 [out2]
 main:6: error: Call to abstract method "meth" of "Base" with trivial body via super() is unsafe
+
+[case testNoCrashDoubleReexportFunctionEmpty]
+import m
+
+[file m.py]
+import f
+[file m.py.3]
+import f
+# modify
+
+[file f.py]
+import c
+def foo(arg: c.C) -> None: pass
+
+[file c.py]
+from types import C
+
+[file types.py]
+import pb1
+C = pb1.C
+[file types.py.2]
+import pb1, pb2
+C = pb2.C
+
+[file pb1.py]
+class C: ...
+[file pb2.py.2]
+class C: ...
+[file pb1.py.2]
+[out]
+[out2]
+[out3]
+
+[case testNoCrashDoubleReexportBaseEmpty]
+import m
+
+[file m.py]
+import f
+[file m.py.3]
+import f
+# modify
+
+[file f.py]
+import c
+class D(c.C): pass
+
+[file c.py]
+from types import C
+
+[file types.py]
+import pb1
+C = pb1.C
+[file types.py.2]
+import pb1, pb2
+C = pb2.C
+
+[file pb1.py]
+class C: ...
+[file pb2.py.2]
+class C: ...
+[file pb1.py.2]
+[out]
+[out2]
+[out3]
+
+[case testNoCrashDoubleReexportMetaEmpty]
+import m
+
+[file m.py]
+import f
+[file m.py.3]
+import f
+# modify
+
+[file f.py]
+import c
+class D(metaclass=c.C): pass
+
+[file c.py]
+from types import C
+
+[file types.py]
+import pb1
+C = pb1.C
+[file types.py.2]
+import pb1, pb2
+C = pb2.C
+
+[file pb1.py]
+class C(type): ...
+[file pb2.py.2]
+class C(type): ...
+[file pb1.py.2]
+[out]
+[out2]
+[out3]
+
+[case testNoCrashDoubleReexportTypedDictEmpty]
+import m
+
+[file m.py]
+import f
+[file m.py.3]
+import f
+# modify
+
+[file f.py]
+from typing_extensions import TypedDict
+import c
+class D(TypedDict):
+    x: c.C
+
+[file c.py]
+from types import C
+
+[file types.py]
+import pb1
+C = pb1.C
+[file types.py.2]
+import pb1, pb2
+C = pb2.C
+
+[file pb1.py]
+class C: ...
+[file pb2.py.2]
+class C: ...
+[file pb1.py.2]
+[builtins fixtures/dict.pyi]
+[out]
+[out2]
+[out3]
+
+[case testNoCrashDoubleReexportTupleEmpty]
+import m
+
+[file m.py]
+import f
+[file m.py.3]
+import f
+# modify
+
+[file f.py]
+from typing import Tuple
+import c
+class D(Tuple[c.C, int]): pass
+
+[file c.py]
+from types import C
+
+[file types.py]
+import pb1
+C = pb1.C
+[file types.py.2]
+import pb1, pb2
+C = pb2.C
+
+[file pb1.py]
+class C: ...
+[file pb2.py.2]
+class C: ...
+[file pb1.py.2]
+[builtins fixtures/tuple.pyi]
+[out]
+[out2]
+[out3]
+
+[case testNoCrashDoubleReexportOverloadEmpty]
+import m
+
+[file m.py]
+import f
+[file m.py.3]
+import f
+# modify
+
+[file f.py]
+from typing import Any, overload
+import c
+
+@overload
+def foo(arg: int) -> None: ...
+@overload
+def foo(arg: c.C) -> None: ...
+def foo(arg: Any) -> None:
+    pass
+
+[file c.py]
+from types import C
+
+[file types.py]
+import pb1
+C = pb1.C
+[file types.py.2]
+import pb1, pb2
+C = pb2.C
+
+[file pb1.py]
+class C: ...
+[file pb2.py.2]
+class C: ...
+[file pb1.py.2]
+[out]
+[out2]
+[out3]