From 303a2230f91c88480079325428ba8e36e4134b73 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 3 Sep 2025 14:07:17 +0100 Subject: [PATCH 01/12] Start work --- mypy/build.py | 4 +- mypy/fixup.py | 2 +- mypy/indirection.py | 57 ++++++++++----------------- mypy/nodes.py | 16 +++++++- mypy/semanal.py | 3 ++ mypy/test/testtypes.py | 4 +- mypy/test/typefixture.py | 6 +-- test-data/unit/check-incremental.test | 34 +++++++++++++++- 8 files changed, 79 insertions(+), 47 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 4ccc3dec408e..db57b0c534a5 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -47,7 +47,7 @@ from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort from mypy.indirection import TypeIndirectionVisitor from mypy.messages import MessageBuilder -from mypy.nodes import Import, ImportAll, ImportBase, ImportFrom, MypyFile, SymbolTable, TypeInfo +from mypy.nodes import Import, ImportAll, ImportBase, ImportFrom, MypyFile, SymbolTable, TypeInfo, TypeAlias from mypy.partially_defined import PossiblyUndefinedVariableVisitor from mypy.semanal import SemanticAnalyzer from mypy.semanal_pass1 import SemanticAnalyzerPreAnalysis @@ -2415,6 +2415,8 @@ def finish_passes(self) -> None: for _, sym, _ in self.tree.local_definitions(): if sym.type is not None: all_types.append(sym.type) + if isinstance(sym.node, TypeAlias): + all_types.append(sym.node.target) if isinstance(sym.node, TypeInfo): # TypeInfo symbols have some extra relevant types. all_types.extend(sym.node.bases) diff --git a/mypy/fixup.py b/mypy/fixup.py index bec5929ad4b1..adf6b34a2b48 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -440,4 +440,4 @@ def missing_info(modules: dict[str, MypyFile]) -> TypeInfo: def missing_alias() -> TypeAlias: suggestion = _SUGGESTION.format("alias") - return TypeAlias(AnyType(TypeOfAny.special_form), suggestion, line=-1, column=-1) + return TypeAlias(AnyType(TypeOfAny.special_form), suggestion, "", line=-1, column=-1) diff --git a/mypy/indirection.py b/mypy/indirection.py index 06a158818fbe..09845d0fa364 100644 --- a/mypy/indirection.py +++ b/mypy/indirection.py @@ -23,44 +23,24 @@ class TypeIndirectionVisitor(TypeVisitor[None]): def __init__(self) -> None: # Module references are collected here self.modules: set[str] = set() - # User to avoid infinite recursion with recursive type aliases - self.seen_aliases: set[types.TypeAliasType] = set() # Used to avoid redundant work self.seen_fullnames: set[str] = set() def find_modules(self, typs: Iterable[types.Type]) -> set[str]: self.modules = set() self.seen_fullnames = set() - self.seen_aliases = set() for typ in typs: - self._visit(typ) + typ.accept(self) return self.modules - def _visit(self, typ: types.Type) -> None: - if isinstance(typ, types.TypeAliasType): - # Avoid infinite recursion for recursive type aliases. - if typ not in self.seen_aliases: - self.seen_aliases.add(typ) - typ.accept(self) - def _visit_type_tuple(self, typs: tuple[types.Type, ...]) -> None: - # Micro-optimization: Specialized version of _visit for lists + # Micro-optimization: Specialized version for lists for typ in typs: - if isinstance(typ, types.TypeAliasType): - # Avoid infinite recursion for recursive type aliases. - if typ in self.seen_aliases: - continue - self.seen_aliases.add(typ) typ.accept(self) def _visit_type_list(self, typs: list[types.Type]) -> None: - # Micro-optimization: Specialized version of _visit for tuples + # Micro-optimization: Specialized version for tuples for typ in typs: - if isinstance(typ, types.TypeAliasType): - # Avoid infinite recursion for recursive type aliases. - if typ in self.seen_aliases: - continue - self.seen_aliases.add(typ) typ.accept(self) def _visit_module_name(self, module_name: str) -> None: @@ -87,16 +67,16 @@ def visit_deleted_type(self, t: types.DeletedType) -> None: def visit_type_var(self, t: types.TypeVarType) -> None: self._visit_type_list(t.values) - self._visit(t.upper_bound) - self._visit(t.default) + t.upper_bound.accept(self) + t.default.accept(self) def visit_param_spec(self, t: types.ParamSpecType) -> None: - self._visit(t.upper_bound) - self._visit(t.default) + t.upper_bound.accept(self) + t.default.accept(self) def visit_type_var_tuple(self, t: types.TypeVarTupleType) -> None: - self._visit(t.upper_bound) - self._visit(t.default) + t.upper_bound.accept(self) + t.default.accept(self) def visit_unpack_type(self, t: types.UnpackType) -> None: t.type.accept(self) @@ -117,7 +97,7 @@ def visit_instance(self, t: types.Instance) -> None: def visit_callable_type(self, t: types.CallableType) -> None: self._visit_type_list(t.arg_types) - self._visit(t.ret_type) + t.ret_type.accept(self) if t.definition is not None: fullname = t.definition.fullname if fullname not in self.seen_fullnames: @@ -125,19 +105,20 @@ def visit_callable_type(self, t: types.CallableType) -> None: self.seen_fullnames.add(fullname) def visit_overloaded(self, t: types.Overloaded) -> None: - self._visit_type_list(list(t.items)) - self._visit(t.fallback) + for typ in t.items: + typ.accept(self) + t.fallback.accept(self) def visit_tuple_type(self, t: types.TupleType) -> None: self._visit_type_list(t.items) - self._visit(t.partial_fallback) + t.partial_fallback.accept(self) def visit_typeddict_type(self, t: types.TypedDictType) -> None: self._visit_type_list(list(t.items.values())) - self._visit(t.fallback) + t.fallback.accept(self) def visit_literal_type(self, t: types.LiteralType) -> None: - self._visit(t.fallback) + t.fallback.accept(self) def visit_union_type(self, t: types.UnionType) -> None: self._visit_type_list(t.items) @@ -146,7 +127,9 @@ def visit_partial_type(self, t: types.PartialType) -> None: pass def visit_type_type(self, t: types.TypeType) -> None: - self._visit(t.item) + t.item.accept(self) def visit_type_alias_type(self, t: types.TypeAliasType) -> None: - self._visit(types.get_proper_type(t)) + if t.alias: + self._visit_module_name(t.alias.module) + self._visit_type_list(t.args) diff --git a/mypy/nodes.py b/mypy/nodes.py index 9cfc61c80b3e..78269ce2c740 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -4121,7 +4121,8 @@ def f(x: B[T]) -> T: ... # without T, Any would be used here target: The target type. For generic aliases contains bound type variables as nested types (currently TypeVar and ParamSpec are supported). _fullname: Qualified name of this type alias. This is used in particular - to track fine grained dependencies from aliases. + to track fine-grained dependencies from aliases. + module: Module where the alias was defined. alias_tvars: Type variables used to define this alias. normalized: Used to distinguish between `A = List`, and `A = list`. Both are internally stored using `builtins.list` (because `typing.List` is @@ -4135,6 +4136,7 @@ def f(x: B[T]) -> T: ... # without T, Any would be used here __slots__ = ( "target", "_fullname", + "module", "alias_tvars", "no_args", "normalized", @@ -4144,12 +4146,13 @@ def f(x: B[T]) -> T: ... # without T, Any would be used here "python_3_12_type_alias", ) - __match_args__ = ("name", "target", "alias_tvars", "no_args") + __match_args__ = ("_fullname", "target", "alias_tvars", "no_args") def __init__( self, target: mypy.types.Type, fullname: str, + module: str, line: int, column: int, *, @@ -4160,6 +4163,7 @@ def __init__( python_3_12_type_alias: bool = False, ) -> None: self._fullname = fullname + self.module = module self.target = target if alias_tvars is None: alias_tvars = [] @@ -4194,6 +4198,7 @@ def from_tuple_type(cls, info: TypeInfo) -> TypeAlias: ) ), info.fullname, + info.module_name, info.line, info.column, ) @@ -4215,6 +4220,7 @@ def from_typeddict_type(cls, info: TypeInfo) -> TypeAlias: ) ), info.fullname, + info.module_name, info.line, info.column, ) @@ -4238,6 +4244,7 @@ def serialize(self) -> JsonDict: data: JsonDict = { ".class": "TypeAlias", "fullname": self._fullname, + "module": self.module, "target": self.target.serialize(), "alias_tvars": [v.serialize() for v in self.alias_tvars], "no_args": self.no_args, @@ -4252,6 +4259,7 @@ def serialize(self) -> JsonDict: def deserialize(cls, data: JsonDict) -> TypeAlias: assert data[".class"] == "TypeAlias" fullname = data["fullname"] + module = data["module"] alias_tvars = [mypy.types.deserialize_type(v) for v in data["alias_tvars"]] assert all(isinstance(t, mypy.types.TypeVarLikeType) for t in alias_tvars) target = mypy.types.deserialize_type(data["target"]) @@ -4263,6 +4271,7 @@ def deserialize(cls, data: JsonDict) -> TypeAlias: return cls( target, fullname, + module, line, column, alias_tvars=cast(list[mypy.types.TypeVarLikeType], alias_tvars), @@ -4274,6 +4283,7 @@ def deserialize(cls, data: JsonDict) -> TypeAlias: def write(self, data: Buffer) -> None: write_tag(data, TYPE_ALIAS) write_str(data, self._fullname) + write_str(data, self.module) self.target.write(data) mypy.types.write_type_list(data, self.alias_tvars) write_int(data, self.line) @@ -4285,11 +4295,13 @@ def write(self, data: Buffer) -> None: @classmethod def read(cls, data: Buffer) -> TypeAlias: fullname = read_str(data) + module = read_str(data) target = mypy.types.read_type(data) alias_tvars = [mypy.types.read_type_var_like(data) for _ in range(read_int(data))] return TypeAlias( target, fullname, + module, read_int(data), read_int(data), alias_tvars=alias_tvars, diff --git a/mypy/semanal.py b/mypy/semanal.py index 50ee3b532463..c6195d8525fd 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -809,6 +809,7 @@ def create_alias(self, tree: MypyFile, target_name: str, alias: str, name: str) alias_node = TypeAlias( target, alias, + tree.fullname, line=-1, column=-1, # there is no context no_args=True, @@ -4106,6 +4107,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: alias_node = TypeAlias( res, self.qualified_name(lvalue.name), + self.cur_mod_id, s.line, s.column, alias_tvars=alias_tvars, @@ -5627,6 +5629,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: alias_node = TypeAlias( res, self.qualified_name(s.name.name), + self.cur_mod_id, s.line, s.column, alias_tvars=alias_tvars, diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 0fe41bc28ecd..82c9b876a340 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -227,13 +227,13 @@ def test_indirection_no_infinite_recursion(self) -> None: visitor = TypeIndirectionVisitor() A.accept(visitor) modules = visitor.modules - assert modules == {"__main__", "builtins"} + assert modules == {"__main__"} A, _ = self.fx.def_alias_2(self.fx.a) visitor = TypeIndirectionVisitor() A.accept(visitor) modules = visitor.modules - assert modules == {"__main__", "builtins"} + assert modules == {"__main__"} class TypeOpsSuite(Suite): diff --git a/mypy/test/typefixture.py b/mypy/test/typefixture.py index 0defcdaebc99..f70c8b94f09c 100644 --- a/mypy/test/typefixture.py +++ b/mypy/test/typefixture.py @@ -374,7 +374,7 @@ def def_alias_1(self, base: Instance) -> tuple[TypeAliasType, Type]: target = Instance( self.std_tuplei, [UnionType([base, A])] ) # A = Tuple[Union[base, A], ...] - AN = TypeAlias(target, "__main__.A", -1, -1) + AN = TypeAlias(target, "__main__.A", "__main__", -1, -1) A.alias = AN return A, target @@ -383,7 +383,7 @@ def def_alias_2(self, base: Instance) -> tuple[TypeAliasType, Type]: target = UnionType( [base, Instance(self.std_tuplei, [A])] ) # A = Union[base, Tuple[A, ...]] - AN = TypeAlias(target, "__main__.A", -1, -1) + AN = TypeAlias(target, "__main__.A", "__main__", -1, -1) A.alias = AN return A, target @@ -393,7 +393,7 @@ def non_rec_alias( alias_tvars: list[TypeVarLikeType] | None = None, args: list[Type] | None = None, ) -> TypeAliasType: - AN = TypeAlias(target, "__main__.A", -1, -1, alias_tvars=alias_tvars) + AN = TypeAlias(target, "__main__.A", "__main__", -1, -1, alias_tvars=alias_tvars) if args is None: args = [] return TypeAliasType(AN, args) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index defe7402730f..db2a92780d79 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6203,6 +6203,38 @@ class C: ... [out2] [out3] +[case testNoCrashDoubleReexportAliasEmpty] +import m + +[file m.py] +import f +[file m.py.3] +import f +# modify + +[file f.py] +import c +D = list[c.C] + +[file c.py] +from types import C + +[file types.py] +import pb1 +C = pb1.C +[file types.py.2] +import 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 @@ -6223,7 +6255,7 @@ from types import C import pb1 C = pb1.C [file types.py.2] -import pb1, pb2 +import pb2 C = pb2.C [file pb1.py] From 95bf9abc36cc58cdf7c31c80266a3cb9ab1be001 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 3 Sep 2025 16:42:34 +0100 Subject: [PATCH 02/12] Some more play --- mypy/build.py | 2 +- test-data/unit/check-incremental.test | 31 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/mypy/build.py b/mypy/build.py index db57b0c534a5..5da7fd5494e5 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -3168,7 +3168,7 @@ def load_graph( # - In this case A's cached *direct* dependencies are still valid # (since direct dependencies reflect the imports found in the source) # but A's cached *indirect* dependency on C is wrong. - dependencies = [dep for dep in st.dependencies if st.priorities.get(dep) != PRI_INDIRECT] + dependencies = [dep for dep in st.dependencies if st.priorities.get(dep) != PRI_INDIRECT or True] if not manager.use_fine_grained_cache(): # TODO: Ideally we could skip here modules that appeared in st.suppressed # because they are not in build with `follow-imports=skip`. diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index db2a92780d79..8fa65005b8d0 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6171,6 +6171,37 @@ class Base: [out2] main:5: error: Call to abstract method "meth" of "Base" with trivial body via super() is unsafe +[case testNoCrashDoubleReexportFunctionUpdated] +import m + +[file m.py] +import f +[file m.py.3] +import f +reveal_type(f.foo) + +[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 pb2 +C = pb2.C + +[file pb1.py] +class C: ... +[file pb2.py] +class C: ... +[out] +[out2] +[out3] + [case testNoCrashDoubleReexportFunctionEmpty] import m From cb7c4cff88d5d391cf06f089df6a89e3aef186b9 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 4 Sep 2025 19:51:31 +0100 Subject: [PATCH 03/12] Add another test --- test-data/unit/check-incremental.test | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 8fa65005b8d0..33d7ed148ea5 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6171,7 +6171,30 @@ class Base: [out2] main:5: error: Call to abstract method "meth" of "Base" with trivial body via super() is unsafe -[case testNoCrashDoubleReexportFunctionUpdated] +[case testLiteralCoarseGrainedChainedAliases] +from mod1 import Alias1 +from typing import Literal +x: Alias1 +def expect_3(x: Literal[3]) -> None: pass +expect_3(x) +[file mod1.py] +from mod2 import Alias2 +Alias1 = Alias2 +[file mod2.py] +from mod3 import Alias3 +Alias2 = Alias3 +[file mod3.py] +from typing import Literal +Alias3 = Literal[3] +[file mod3.py.2] +from typing import Literal +Alias3 = Literal[4] +[builtins fixtures/tuple.pyi] +[out] +[out2] +main:5: error: Argument 1 to "expect_3" has incompatible type "Literal[4]"; expected "Literal[3]" + +[case testDoubleReexportFunctionUpdated] import m [file m.py] From e9ccd55aebe771a65292ab63266bd436019bb778 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 5 Sep 2025 00:25:03 +0100 Subject: [PATCH 04/12] Move towards real deal --- mypy/build.py | 13 +++++- mypy/indirection.py | 54 +++++++++++++++------- mypy/test/testtypes.py | 4 +- test-data/unit/check-incremental.test | 65 +++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 21 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index 5da7fd5494e5..cffb451a8ef2 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -47,7 +47,16 @@ from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort from mypy.indirection import TypeIndirectionVisitor from mypy.messages import MessageBuilder -from mypy.nodes import Import, ImportAll, ImportBase, ImportFrom, MypyFile, SymbolTable, TypeInfo, TypeAlias +from mypy.nodes import ( + Import, + ImportAll, + ImportBase, + ImportFrom, + MypyFile, + SymbolTable, + TypeAlias, + TypeInfo, +) from mypy.partially_defined import PossiblyUndefinedVariableVisitor from mypy.semanal import SemanticAnalyzer from mypy.semanal_pass1 import SemanticAnalyzerPreAnalysis @@ -3168,7 +3177,7 @@ def load_graph( # - In this case A's cached *direct* dependencies are still valid # (since direct dependencies reflect the imports found in the source) # but A's cached *indirect* dependency on C is wrong. - dependencies = [dep for dep in st.dependencies if st.priorities.get(dep) != PRI_INDIRECT or True] + dependencies = [dep for dep in st.dependencies if st.priorities.get(dep) != PRI_INDIRECT] if not manager.use_fine_grained_cache(): # TODO: Ideally we could skip here modules that appeared in st.suppressed # because they are not in build with `follow-imports=skip`. diff --git a/mypy/indirection.py b/mypy/indirection.py index 09845d0fa364..9e77e7a0d889 100644 --- a/mypy/indirection.py +++ b/mypy/indirection.py @@ -23,24 +23,44 @@ class TypeIndirectionVisitor(TypeVisitor[None]): def __init__(self) -> None: # Module references are collected here self.modules: set[str] = set() + # User to avoid infinite recursion with recursive type aliases + self.seen_aliases: set[types.TypeAliasType] = set() # Used to avoid redundant work self.seen_fullnames: set[str] = set() def find_modules(self, typs: Iterable[types.Type]) -> set[str]: self.modules = set() self.seen_fullnames = set() + self.seen_aliases = set() for typ in typs: - typ.accept(self) + self._visit(typ) return self.modules + def _visit(self, typ: types.Type) -> None: + if isinstance(typ, types.TypeAliasType): + # Avoid infinite recursion for recursive type aliases. + if typ not in self.seen_aliases: + self.seen_aliases.add(typ) + typ.accept(self) + def _visit_type_tuple(self, typs: tuple[types.Type, ...]) -> None: - # Micro-optimization: Specialized version for lists + # Micro-optimization: Specialized version of _visit for lists for typ in typs: + if isinstance(typ, types.TypeAliasType): + # Avoid infinite recursion for recursive type aliases. + if typ in self.seen_aliases: + continue + self.seen_aliases.add(typ) typ.accept(self) def _visit_type_list(self, typs: list[types.Type]) -> None: - # Micro-optimization: Specialized version for tuples + # Micro-optimization: Specialized version of _visit for tuples for typ in typs: + if isinstance(typ, types.TypeAliasType): + # Avoid infinite recursion for recursive type aliases. + if typ in self.seen_aliases: + continue + self.seen_aliases.add(typ) typ.accept(self) def _visit_module_name(self, module_name: str) -> None: @@ -67,16 +87,16 @@ def visit_deleted_type(self, t: types.DeletedType) -> None: def visit_type_var(self, t: types.TypeVarType) -> None: self._visit_type_list(t.values) - t.upper_bound.accept(self) - t.default.accept(self) + self._visit(t.upper_bound) + self._visit(t.default) def visit_param_spec(self, t: types.ParamSpecType) -> None: - t.upper_bound.accept(self) - t.default.accept(self) + self._visit(t.upper_bound) + self._visit(t.default) def visit_type_var_tuple(self, t: types.TypeVarTupleType) -> None: - t.upper_bound.accept(self) - t.default.accept(self) + self._visit(t.upper_bound) + self._visit(t.default) def visit_unpack_type(self, t: types.UnpackType) -> None: t.type.accept(self) @@ -97,7 +117,7 @@ def visit_instance(self, t: types.Instance) -> None: def visit_callable_type(self, t: types.CallableType) -> None: self._visit_type_list(t.arg_types) - t.ret_type.accept(self) + self._visit(t.ret_type) if t.definition is not None: fullname = t.definition.fullname if fullname not in self.seen_fullnames: @@ -105,20 +125,19 @@ def visit_callable_type(self, t: types.CallableType) -> None: self.seen_fullnames.add(fullname) def visit_overloaded(self, t: types.Overloaded) -> None: - for typ in t.items: - typ.accept(self) - t.fallback.accept(self) + self._visit_type_list(list(t.items)) + self._visit(t.fallback) def visit_tuple_type(self, t: types.TupleType) -> None: self._visit_type_list(t.items) - t.partial_fallback.accept(self) + self._visit(t.partial_fallback) def visit_typeddict_type(self, t: types.TypedDictType) -> None: self._visit_type_list(list(t.items.values())) - t.fallback.accept(self) + self._visit(t.fallback) def visit_literal_type(self, t: types.LiteralType) -> None: - t.fallback.accept(self) + self._visit(t.fallback) def visit_union_type(self, t: types.UnionType) -> None: self._visit_type_list(t.items) @@ -127,9 +146,10 @@ def visit_partial_type(self, t: types.PartialType) -> None: pass def visit_type_type(self, t: types.TypeType) -> None: - t.item.accept(self) + self._visit(t.item) def visit_type_alias_type(self, t: types.TypeAliasType) -> None: if t.alias: self._visit_module_name(t.alias.module) + self._visit(t.alias.target) self._visit_type_list(t.args) diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 82c9b876a340..0fe41bc28ecd 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -227,13 +227,13 @@ def test_indirection_no_infinite_recursion(self) -> None: visitor = TypeIndirectionVisitor() A.accept(visitor) modules = visitor.modules - assert modules == {"__main__"} + assert modules == {"__main__", "builtins"} A, _ = self.fx.def_alias_2(self.fx.a) visitor = TypeIndirectionVisitor() A.accept(visitor) modules = visitor.modules - assert modules == {"__main__"} + assert modules == {"__main__", "builtins"} class TypeOpsSuite(Suite): diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 33d7ed148ea5..8557f060bf61 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6224,6 +6224,7 @@ class C: ... [out] [out2] [out3] +tmp/m.py:2: note: Revealed type is "def (arg: pb2.C)" [case testNoCrashDoubleReexportFunctionEmpty] import m @@ -6305,6 +6306,38 @@ 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 testNoCrashDoubleReexportBaseEmpty2] +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 @@ -6353,6 +6386,38 @@ class C(type): ... [out2] [out3] +[case testNoCrashDoubleReexportMetaEmpty2] +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 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 From 7cf8411ad860ddc63dd13c46c66d4363b5758573 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 5 Sep 2025 00:32:47 +0100 Subject: [PATCH 05/12] Minor undo --- mypy/nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 78269ce2c740..71b67bd71d99 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -4146,7 +4146,7 @@ def f(x: B[T]) -> T: ... # without T, Any would be used here "python_3_12_type_alias", ) - __match_args__ = ("_fullname", "target", "alias_tvars", "no_args") + __match_args__ = ("name", "target", "alias_tvars", "no_args") def __init__( self, From 4f682eb39414d6fec39d5e9b008121f5616283de Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 5 Sep 2025 14:08:09 +0100 Subject: [PATCH 06/12] Finish main part --- mypy/build.py | 70 +++++++++++++------------- mypy/indirection.py | 71 +++++++++++++++++++-------- mypy/nodes.py | 4 ++ mypy/semanal.py | 48 +++++++----------- mypy/server/deps.py | 15 +++--- mypy/typeanal.py | 11 +++-- test-data/unit/check-incremental.test | 36 +++++++++++++- 7 files changed, 155 insertions(+), 100 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index cffb451a8ef2..b043cc8bb4b9 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -48,14 +48,14 @@ from mypy.indirection import TypeIndirectionVisitor from mypy.messages import MessageBuilder from mypy.nodes import ( + Decorator, Import, ImportAll, ImportBase, ImportFrom, MypyFile, + OverloadedFuncDef, SymbolTable, - TypeAlias, - TypeInfo, ) from mypy.partially_defined import PossiblyUndefinedVariableVisitor from mypy.semanal import SemanticAnalyzer @@ -1767,26 +1767,23 @@ def delete_cache(id: str, path: str, manager: BuildManager) -> None: For single nodes, processing is simple. If the node was cached, we deserialize the cache data and fix up cross-references. Otherwise, we -do semantic analysis followed by type checking. We also handle (c) -above; if a module has valid cache data *but* any of its -dependencies was processed from source, then the module should be -processed from source. - -A relatively simple optimization (outside SCCs) we might do in the -future is as follows: if a node's cache data is valid, but one or more -of its dependencies are out of date so we have to re-parse the node -from source, once we have fully type-checked the node, we can decide -whether its symbol table actually changed compared to the cache data -(by reading the cache data and comparing it to the data we would be -writing). If there is no change we can declare the node up to date, -and any node that depends (and for which we have cached data, and -whose other dependencies are up to date) on it won't need to be -re-parsed from source. +do semantic analysis followed by type checking. Once we (re-)processed +an SCC we check whether its interface (symbol table) is still fresh +(matches previous cached value). If it is not, we consider dependent SCCs +stale so that they need to be re-parsed as well. + +Note on indirect dependencies: normally dependencies are determined from +imports, but since our type interfaces are "opaque" (i.e. symbol tables can +contain types identified by name), these are not enough. We *must* also +add "indirect" dependencies from types to their definitions. For this +purpose, after we finished processing a module, we travers its type map and +symbol tables, and for each type we find (transitively) on which opaque/named +types it depends. Import cycles ------------- -Finally we have to decide how to handle (c), import cycles. Here +Finally we have to decide how to handle (b), import cycles. Here we'll need a modified version of the original state machine (build.py), but we only need to do this per SCC, and we won't have to deal with changes to the list of nodes while we're processing it. @@ -2419,23 +2416,23 @@ 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. - # TODO: find a more robust way to traverse *all* relevant types? - all_types = list(self.type_map().values()) + all_types = set(self.type_map().values()) for _, sym, _ in self.tree.local_definitions(): if sym.type is not None: - all_types.append(sym.type) - if isinstance(sym.node, TypeAlias): - all_types.append(sym.node.target) - if isinstance(sym.node, TypeInfo): - # TypeInfo symbols have some extra relevant types. - all_types.extend(sym.node.bases) - if sym.node.metaclass_type: - all_types.append(sym.node.metaclass_type) - if sym.node.typeddict_type: - all_types.append(sym.node.typeddict_type) - if sym.node.tuple_type: - all_types.append(sym.node.tuple_type) - self._patch_indirect_dependencies(self.type_checker().module_refs, all_types) + all_types.add(sym.type) + # Special case: settable properties may have two types. + if isinstance(sym.node, OverloadedFuncDef) and sym.node.is_property: + assert isinstance(first_node := sym.node.items[0], Decorator) + if first_node.var.setter_type: + all_types.add(first_node.var.setter_type) + # Using mod_alias_deps is unfortunate but needed, since it is highly impractical + # (and practically impossible) to avoid all get_proper_type() calls. For example, + # TypeInfo.bases and metaclass, *args and **kwargs, Overloaded.items, and trivial + # aliases like Text = str, etc. all currently forced to proper types. Thus, we need + # to record the original definitions as they are first seen in semanal.py. + self._patch_indirect_dependencies( + self.type_checker().module_refs | self.tree.mod_alias_deps, all_types + ) if self.options.dump_inference_stats: dump_type_stats( @@ -2464,7 +2461,7 @@ def free_state(self) -> None: self._type_checker.reset() self._type_checker = None - def _patch_indirect_dependencies(self, module_refs: set[str], types: list[Type]) -> None: + def _patch_indirect_dependencies(self, module_refs: set[str], types: set[Type]) -> None: assert None not in types valid = self.valid_references() @@ -3298,7 +3295,10 @@ def process_graph(graph: Graph, manager: BuildManager) -> None: for id in scc: deps.update(graph[id].dependencies) deps -= ascc - stale_deps = {id for id in deps if id in graph and not graph[id].is_interface_fresh()} + # Note: if a dependency is not in graph anymore, it should be considered interface-stale. + # This is important to trigger any relevant updates from indirect dependencies that were + # removed in load_graph(). + stale_deps = {id for id in deps if id not in graph or not graph[id].is_interface_fresh()} fresh = fresh and not stale_deps undeps = set() if fresh: diff --git a/mypy/indirection.py b/mypy/indirection.py index 9e77e7a0d889..1bbbb2a7b075 100644 --- a/mypy/indirection.py +++ b/mypy/indirection.py @@ -23,44 +23,58 @@ class TypeIndirectionVisitor(TypeVisitor[None]): def __init__(self) -> None: # Module references are collected here self.modules: set[str] = set() - # User to avoid infinite recursion with recursive type aliases - self.seen_aliases: set[types.TypeAliasType] = set() + # User to avoid infinite recursion with recursive types + self.seen_types: set[types.TypeAliasType | types.Instance] = set() # Used to avoid redundant work self.seen_fullnames: set[str] = set() def find_modules(self, typs: Iterable[types.Type]) -> set[str]: self.modules = set() self.seen_fullnames = set() - self.seen_aliases = set() + self.seen_types = set() for typ in typs: self._visit(typ) return self.modules def _visit(self, typ: types.Type) -> None: - if isinstance(typ, types.TypeAliasType): - # Avoid infinite recursion for recursive type aliases. - if typ not in self.seen_aliases: - self.seen_aliases.add(typ) + # Note: instances are needed for `class str(Sequence[str]): ...` + if ( + isinstance(typ, types.TypeAliasType) + or isinstance(typ, types.ProperType) + and isinstance(typ, types.Instance) + ): + # Avoid infinite recursion for recursive types. + if typ in self.seen_types: + return + self.seen_types.add(typ) typ.accept(self) def _visit_type_tuple(self, typs: tuple[types.Type, ...]) -> None: # Micro-optimization: Specialized version of _visit for lists for typ in typs: - if isinstance(typ, types.TypeAliasType): - # Avoid infinite recursion for recursive type aliases. - if typ in self.seen_aliases: + if ( + isinstance(typ, types.TypeAliasType) + or isinstance(typ, types.ProperType) + and isinstance(typ, types.Instance) + ): + # Avoid infinite recursion for recursive types. + if typ in self.seen_types: continue - self.seen_aliases.add(typ) + self.seen_types.add(typ) typ.accept(self) def _visit_type_list(self, typs: list[types.Type]) -> None: # Micro-optimization: Specialized version of _visit for tuples for typ in typs: - if isinstance(typ, types.TypeAliasType): - # Avoid infinite recursion for recursive type aliases. - if typ in self.seen_aliases: + if ( + isinstance(typ, types.TypeAliasType) + or isinstance(typ, types.ProperType) + and isinstance(typ, types.Instance) + ): + # Avoid infinite recursion for recursive types. + if typ in self.seen_types: continue - self.seen_aliases.add(typ) + self.seen_types.add(typ) typ.accept(self) def _visit_module_name(self, module_name: str) -> None: @@ -105,15 +119,27 @@ def visit_parameters(self, t: types.Parameters) -> None: self._visit_type_list(t.arg_types) def visit_instance(self, t: types.Instance) -> None: + # Instance is named, record its definition and continue digging into + # components that constitute semantic meaning of this type: bases, metaclass, + # tuple type, and typeddict type. + # Note: we cannot simply record the MRO, in case an intermediate base contains + # a reference to type alias, this affects meaning of map_instance_to_supertype(), + # see e.g. testDoubleReexportGenericUpdated. self._visit_type_tuple(t.args) if t.type: - # Uses of a class depend on everything in the MRO, - # as changes to classes in the MRO can add types to methods, - # change property types, change the MRO itself, etc. + # Important optimization: instead of simply recording the definition and + # recursing into bases, record the MRO and only traverse generic bases. for s in t.type.mro: self._visit_module_name(s.module_name) - if t.type.metaclass_type is not None: - self._visit_module_name(t.type.metaclass_type.type.module_name) + for base in s.bases: + if base.args: + self._visit_type_tuple(base.args) + if t.type.metaclass_type: + self._visit(t.type.metaclass_type) + if t.type.typeddict_type: + self._visit(t.type.typeddict_type) + if t.type.tuple_type: + self._visit(t.type.tuple_type) def visit_callable_type(self, t: types.CallableType) -> None: self._visit_type_list(t.arg_types) @@ -125,7 +151,8 @@ def visit_callable_type(self, t: types.CallableType) -> None: self.seen_fullnames.add(fullname) def visit_overloaded(self, t: types.Overloaded) -> None: - self._visit_type_list(list(t.items)) + for item in t.items: + self._visit(item) self._visit(t.fallback) def visit_tuple_type(self, t: types.TupleType) -> None: @@ -149,6 +176,8 @@ def visit_type_type(self, t: types.TypeType) -> None: self._visit(t.item) def visit_type_alias_type(self, t: types.TypeAliasType) -> None: + # Type alias is named, record its definition and continue digging into + # components that constitute semantic meaning of this type: target and args. if t.alias: self._visit_module_name(t.alias.module) self._visit(t.alias.target) diff --git a/mypy/nodes.py b/mypy/nodes.py index 71b67bd71d99..d137cd814a2b 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -288,6 +288,7 @@ class MypyFile(SymbolNode): "path", "defs", "alias_deps", + "mod_alias_deps", "is_bom", "names", "imports", @@ -311,6 +312,8 @@ class MypyFile(SymbolNode): defs: list[Statement] # Type alias dependencies as mapping from target to set of alias full names alias_deps: defaultdict[str, set[str]] + # Same as above but for coarse-grained dependencies (i.e. modules instead of full names) + mod_alias_deps: set[str] # Is there a UTF-8 BOM at the start? is_bom: bool names: SymbolTable @@ -351,6 +354,7 @@ def __init__( self.imports = imports self.is_bom = is_bom self.alias_deps = defaultdict(set) + self.mod_alias_deps = set() self.plugin_deps = {} if ignored_lines: self.ignored_lines = ignored_lines diff --git a/mypy/semanal.py b/mypy/semanal.py index c6195d8525fd..1f1e0e82c8eb 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2808,6 +2808,7 @@ def get_declared_metaclass( and not sym.node.alias_tvars ): target = get_proper_type(sym.node.target) + self.add_type_alias_deps({(sym.node.module, sym.node.fullname)}) if isinstance(target, Instance): metaclass_info = target.type @@ -3886,16 +3887,15 @@ def analyze_alias( declared_type_vars: TypeVarLikeList | None = None, all_declared_type_params_names: list[str] | None = None, python_3_12_type_alias: bool = False, - ) -> tuple[Type | None, list[TypeVarLikeType], set[str], list[str], bool]: + ) -> tuple[Type | None, list[TypeVarLikeType], set[tuple[str, str]], bool]: """Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable). - If yes, return the corresponding type, a list of - qualified type variable names for generic aliases, a set of names the alias depends on, - and a list of type variables if the alias is generic. - A schematic example for the dependencies: + If yes, return the corresponding type, a list of type variables for generic aliases, + a set of names the alias depends on, and True if the original type has empty tuple index. + An example for the dependencies: A = int B = str - analyze_alias(Dict[A, B])[2] == {'__main__.A', '__main__.B'} + analyze_alias(dict[A, B])[2] == {('mod', 'mod.A'), ('mod', 'mod.B')} """ dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic()) global_scope = not self.type and not self.function_stack @@ -3907,10 +3907,9 @@ def analyze_alias( self.fail( "Invalid type alias: expression is not a valid type", rvalue, code=codes.VALID_TYPE ) - return None, [], set(), [], False + return None, [], set(), False found_type_vars = self.find_type_var_likes(typ) - tvar_defs: list[TypeVarLikeType] = [] namespace = self.qualified_name(name) alias_type_vars = found_type_vars if declared_type_vars is None else declared_type_vars with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)): @@ -3946,9 +3945,8 @@ def analyze_alias( variadic = True new_tvar_defs.append(td) - qualified_tvars = [node.fullname for _name, node in alias_type_vars] empty_tuple_index = typ.empty_tuple_index if isinstance(typ, UnboundType) else False - return analyzed, new_tvar_defs, depends_on, qualified_tvars, empty_tuple_index + return analyzed, new_tvar_defs, depends_on, empty_tuple_index def is_pep_613(self, s: AssignmentStmt) -> bool: if s.unanalyzed_type is not None and isinstance(s.unanalyzed_type, UnboundType): @@ -4042,12 +4040,11 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: if self.is_none_alias(rvalue): res = NoneType() alias_tvars: list[TypeVarLikeType] = [] - depends_on: set[str] = set() - qualified_tvars: list[str] = [] + depends_on: set[tuple[str, str]] = set() empty_tuple_index = False else: tag = self.track_incomplete_refs() - res, alias_tvars, depends_on, qualified_tvars, empty_tuple_index = self.analyze_alias( + res, alias_tvars, depends_on, empty_tuple_index = self.analyze_alias( lvalue.name, rvalue, allow_placeholder=True, @@ -4071,12 +4068,6 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: self.mark_incomplete(lvalue.name, rvalue, becomes_typeinfo=True) return True self.add_type_alias_deps(depends_on) - # In addition to the aliases used, we add deps on unbound - # type variables, since they are erased from target type. - self.add_type_alias_deps(qualified_tvars) - # The above are only direct deps on other aliases. - # For subscripted aliases, type deps from expansion are added in deps.py - # (because the type is stored). check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg, context=s) # When this type alias gets "inlined", the Any is not explicit anymore, # so we need to replace it with non-explicit Anys. @@ -5579,7 +5570,7 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: return tag = self.track_incomplete_refs() - res, alias_tvars, depends_on, qualified_tvars, empty_tuple_index = self.analyze_alias( + res, alias_tvars, depends_on, empty_tuple_index = self.analyze_alias( s.name.name, s.value.expr(), allow_placeholder=True, @@ -5608,12 +5599,6 @@ def visit_type_alias_stmt(self, s: TypeAliasStmt) -> None: return self.add_type_alias_deps(depends_on) - # In addition to the aliases used, we add deps on unbound - # type variables, since they are erased from target type. - self.add_type_alias_deps(qualified_tvars) - # The above are only direct deps on other aliases. - # For subscripted aliases, type deps from expansion are added in deps.py - # (because the type is stored). check_for_explicit_any( res, self.options, self.is_typeshed_stub_file, self.msg, context=s ) @@ -7541,20 +7526,21 @@ def add_plugin_dependency(self, trigger: str, target: str | None = None) -> None self.cur_mod_node.plugin_deps.setdefault(trigger, set()).add(target) def add_type_alias_deps( - self, aliases_used: Collection[str], target: str | None = None + self, aliases_used: Collection[tuple[str, str]], target: str | None = None ) -> None: """Add full names of type aliases on which the current node depends. This is used by fine-grained incremental mode to re-check the corresponding nodes. - If `target` is None, then the target node used will be the current scope. + If `target` is None, then the target node used will be the current scope. For + coarse-grained mode, add just the module names where aliases are defined. """ if not aliases_used: - # A basic optimization to avoid adding targets with no dependencies to - # the `alias_deps` dict. return if target is None: target = self.scope.current_target() - self.cur_mod_node.alias_deps[target].update(aliases_used) + for mod, fn in aliases_used: + self.cur_mod_node.alias_deps[target].add(fn) + self.cur_mod_node.mod_alias_deps.add(mod) def is_mangled_global(self, name: str) -> bool: # A global is mangled if there exists at least one renamed variant. diff --git a/mypy/server/deps.py b/mypy/server/deps.py index b994a214f67a..3ebba36fc713 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -227,14 +227,17 @@ def __init__( self.scope = Scope() self.type_map = type_map # This attribute holds a mapping from target to names of type aliases - # it depends on. These need to be processed specially, since they are - # only present in expanded form in symbol tables. For example, after: - # A = List[int] + # it depends on. These need to be processed specially, since they may + # appear in expanded form in symbol tables, because of a get_proper_type() + # somewhere. For example, after: + # A = int # x: A - # The module symbol table will just have a Var `x` with type `List[int]`, - # and the dependency of `x` on `A` is lost. Therefore the alias dependencies + # the module symbol table will just have a Var `x` with type `int`, + # and the dependency of `x` on `A` is lost. Therefore, the alias dependencies # are preserved at alias expansion points in `semanal.py`, stored as an attribute # on MypyFile, and then passed here. + # TODO: fine-grained is more susceptible to this partially because we are reckless + # about get_proper_type() in *this specific file*. self.alias_deps = alias_deps self.map: dict[str, set[str]] = {} self.is_class = False @@ -979,8 +982,6 @@ def visit_type_alias_type(self, typ: TypeAliasType) -> list[str]: triggers = [trigger] for arg in typ.args: triggers.extend(self.get_type_triggers(arg)) - # TODO: Now that type aliases are its own kind of types we can simplify - # the logic to rely on intermediate dependencies (like for instance types). triggers.extend(self.get_type_triggers(typ.alias.target)) return triggers diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 7429030573a3..22a015212f8f 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -155,11 +155,11 @@ def analyze_type_alias( allowed_alias_tvars: list[TypeVarLikeType] | None = None, alias_type_params_names: list[str] | None = None, python_3_12_type_alias: bool = False, -) -> tuple[Type, set[str]]: +) -> tuple[Type, set[tuple[str, str]]]: """Analyze r.h.s. of a (potential) type alias definition. If `node` is valid as a type alias rvalue, return the resulting type and a set of - full names of type aliases it depends on (directly or indirectly). + module and full names of type aliases it depends on (directly or indirectly). 'node' must have been semantically analyzed. """ analyzer = TypeAnalyser( @@ -263,8 +263,9 @@ def __init__( self.options = options self.cur_mod_node = cur_mod_node self.is_typeshed_stub = is_typeshed_stub - # Names of type aliases encountered while analysing a type will be collected here. - self.aliases_used: set[str] = set() + # Names of type aliases encountered while analysing a type will be collected here + # (each tuple in the set is (module_name, fullname)). + self.aliases_used: set[tuple[str, str]] = set() self.prohibit_self_type = prohibit_self_type # Set when we analyze TypedDicts or NamedTuples, since they are special: self.prohibit_special_class_field_types = prohibit_special_class_field_types @@ -457,7 +458,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) if special is not None: return special if isinstance(node, TypeAlias): - self.aliases_used.add(fullname) + self.aliases_used.add((node.module, fullname)) an_args = self.anal_array( t.args, allow_param_spec=True, diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 8557f060bf61..9018d0f35a99 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6214,7 +6214,7 @@ from types import C import pb1 C = pb1.C [file types.py.2] -import pb2 +import pb1, pb2 C = pb2.C [file pb1.py] @@ -6226,6 +6226,40 @@ class C: ... [out3] tmp/m.py:2: note: Revealed type is "def (arg: pb2.C)" +[case testDoubleReexportGenericUpdated] +import m + +[file m.py] +import f +[file m.py.3] +import f +x: f.F +reveal_type(x[0]) + +[file f.py] +import c +class FB(list[c.C]): ... +class F(FB): ... + +[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] +class C: ... +[out] +[out2] +[out3] +tmp/m.py:3: note: Revealed type is "pb2.C" + [case testNoCrashDoubleReexportFunctionEmpty] import m From 49f8475a32214a5fe0cfec838b4c6086460efc57 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 5 Sep 2025 14:25:21 +0100 Subject: [PATCH 07/12] One more tests (just in case) --- test-data/unit/check-incremental.test | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 9018d0f35a99..35fe90ad2231 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6175,6 +6175,29 @@ main:5: error: Call to abstract method "meth" of "Base" with trivial body via su from mod1 import Alias1 from typing import Literal x: Alias1 +def expect_int(x: int) -> None: pass +expect_int(x) +[file mod1.py] +from mod2 import Alias2 +Alias1 = Alias2 +[file mod2.py] +from mod3 import Alias3 +Alias2 = Alias3 +[file mod3.py] +from typing import Literal +Alias3 = int +[file mod3.py.2] +from typing import Literal +Alias3 = str +[builtins fixtures/tuple.pyi] +[out] +[out2] +main:5: error: Argument 1 to "expect_int" has incompatible type "str"; expected "int" + +[case testLiteralCoarseGrainedChainedAliases2] +from mod1 import Alias1 +from typing import Literal +x: Alias1 def expect_3(x: Literal[3]) -> None: pass expect_3(x) [file mod1.py] From e05e5189a401f48fcc7068f830ea51b617c16aba Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 6 Sep 2025 17:01:51 +0100 Subject: [PATCH 08/12] Incorporate more fixes --- mypy/build.py | 46 ++++++++---------------- mypy/checker.py | 6 ++-- mypy/checkexpr.py | 33 ------------------ mypy/checkmember.py | 2 ++ mypy/indirection.py | 19 +--------- mypy/nodes.py | 9 ++--- mypy/semanal.py | 50 ++++++++++++++++++++------- mypy/typeanal.py | 11 +++--- test-data/unit/check-incremental.test | 31 +++++++++++++++++ 9 files changed, 98 insertions(+), 109 deletions(-) diff --git a/mypy/build.py b/mypy/build.py index b043cc8bb4b9..0b9d5fc4fc09 100644 --- a/mypy/build.py +++ b/mypy/build.py @@ -47,16 +47,7 @@ from mypy.graph_utils import prepare_sccs, strongly_connected_components, topsort from mypy.indirection import TypeIndirectionVisitor from mypy.messages import MessageBuilder -from mypy.nodes import ( - Decorator, - Import, - ImportAll, - ImportBase, - ImportFrom, - MypyFile, - OverloadedFuncDef, - SymbolTable, -) +from mypy.nodes import Import, ImportAll, ImportBase, ImportFrom, MypyFile, SymbolTable from mypy.partially_defined import PossiblyUndefinedVariableVisitor from mypy.semanal import SemanticAnalyzer from mypy.semanal_pass1 import SemanticAnalyzerPreAnalysis @@ -1773,12 +1764,13 @@ def delete_cache(id: str, path: str, manager: BuildManager) -> None: stale so that they need to be re-parsed as well. Note on indirect dependencies: normally dependencies are determined from -imports, but since our type interfaces are "opaque" (i.e. symbol tables can -contain types identified by name), these are not enough. We *must* also -add "indirect" dependencies from types to their definitions. For this -purpose, after we finished processing a module, we travers its type map and -symbol tables, and for each type we find (transitively) on which opaque/named -types it depends. +imports, but since our interfaces are "opaque" (i.e. symbol tables can +contain cross-references as well as types identified by name), these are not +enough. We *must* also add "indirect" dependencies from symbols and types to +their definitions. For this purpose, we record all accessed symbols during +semantic analysis, and after we finished processing a module, we traverse its +type map, and for each type we find (transitively) on which named types it +depends. Import cycles ------------- @@ -2416,22 +2408,14 @@ 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. - all_types = set(self.type_map().values()) - for _, sym, _ in self.tree.local_definitions(): - if sym.type is not None: - all_types.add(sym.type) - # Special case: settable properties may have two types. - if isinstance(sym.node, OverloadedFuncDef) and sym.node.is_property: - assert isinstance(first_node := sym.node.items[0], Decorator) - if first_node.var.setter_type: - all_types.add(first_node.var.setter_type) - # Using mod_alias_deps is unfortunate but needed, since it is highly impractical - # (and practically impossible) to avoid all get_proper_type() calls. For example, - # TypeInfo.bases and metaclass, *args and **kwargs, Overloaded.items, and trivial - # aliases like Text = str, etc. all currently forced to proper types. Thus, we need - # to record the original definitions as they are first seen in semanal.py. self._patch_indirect_dependencies( - self.type_checker().module_refs | self.tree.mod_alias_deps, all_types + # Two possible sources of indirect dependencies: + # * Symbols not directly imported in this module but accessed via an attribute + # or via a re-export (vast majority of these recorded in semantic analysis). + # * For each expression type we need to record definitions of type components + # since "meaning" of the type may be updated when definitions are updated. + self.tree.module_refs | self.type_checker().module_refs, + set(self.type_map().values()), ) if self.options.dump_inference_stats: diff --git a/mypy/checker.py b/mypy/checker.py index ba821df621e5..eabf839ec517 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -378,11 +378,9 @@ class TypeChecker(NodeVisitor[None], TypeCheckerSharedApi): inferred_attribute_types: dict[Var, Type] | None = None # Don't infer partial None types if we are processing assignment from Union no_partial_types: bool = False - - # The set of all dependencies (suppressed or not) that this module accesses, either - # directly or indirectly. + # Extra module references not detected during semantic analysis (these are rare cases + # e.g. access to class-level import via instance). module_refs: set[str] - # A map from variable nodes to a snapshot of the frame ids of the # frames that were active when the variable was declared. This can # be used to determine nearest common ancestor frame of a variable's diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 835eeb725394..73282c94be4e 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -198,7 +198,6 @@ ) from mypy.typestate import type_state from mypy.typevars import fill_typevars -from mypy.util import split_module_names from mypy.visitor import ExpressionVisitor # Type of callback user for checking individual function arguments. See @@ -248,36 +247,6 @@ def allow_fast_container_literal(t: Type) -> bool: ) -def extract_refexpr_names(expr: RefExpr, output: set[str]) -> None: - """Recursively extracts all module references from a reference expression. - - Note that currently, the only two subclasses of RefExpr are NameExpr and - MemberExpr.""" - while isinstance(expr.node, MypyFile) or expr.fullname: - if isinstance(expr.node, MypyFile) and expr.fullname: - # If it's None, something's wrong (perhaps due to an - # import cycle or a suppressed error). For now we just - # skip it. - output.add(expr.fullname) - - if isinstance(expr, NameExpr): - is_suppressed_import = isinstance(expr.node, Var) and expr.node.is_suppressed_import - if isinstance(expr.node, TypeInfo): - # Reference to a class or a nested class - output.update(split_module_names(expr.node.module_name)) - elif "." in expr.fullname and not is_suppressed_import: - # Everything else (that is not a silenced import within a class) - output.add(expr.fullname.rsplit(".", 1)[0]) - break - elif isinstance(expr, MemberExpr): - if isinstance(expr.expr, RefExpr): - expr = expr.expr - else: - break - else: - raise AssertionError(f"Unknown RefExpr subclass: {type(expr)}") - - class Finished(Exception): """Raised if we can terminate overload argument check early (no match).""" @@ -370,7 +339,6 @@ def visit_name_expr(self, e: NameExpr) -> Type: It can be of any kind: local, member or global. """ - extract_refexpr_names(e, self.chk.module_refs) result = self.analyze_ref_expr(e) narrowed = self.narrow_type_from_binder(e, result) self.chk.check_deprecated(e.node, e) @@ -3344,7 +3312,6 @@ def check_union_call( def visit_member_expr(self, e: MemberExpr, is_lvalue: bool = False) -> Type: """Visit member expression (of form e.id).""" - extract_refexpr_names(e, self.chk.module_refs) result = self.analyze_ordinary_member_access(e, is_lvalue) narrowed = self.narrow_type_from_binder(e, result) self.chk.warn_deprecated(e.node, e) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index e7de1b7a304f..f19a76ec6a34 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -543,6 +543,8 @@ def analyze_member_var_access( if isinstance(v, FuncDef): assert False, "Did not expect a function" if isinstance(v, MypyFile): + # Special case: accessing module on instances is allowed, but will not + # be recorded by semantic analyzer. mx.chk.module_refs.add(v.fullname) if isinstance(vv, (TypeInfo, TypeAlias, MypyFile, TypeVarLikeExpr)): diff --git a/mypy/indirection.py b/mypy/indirection.py index 75eeb07b266c..36fa305653c1 100644 --- a/mypy/indirection.py +++ b/mypy/indirection.py @@ -7,16 +7,6 @@ from mypy.util import split_module_names -def extract_module_names(type_name: str | None) -> list[str]: - """Returns the module names of a fully qualified type name.""" - if type_name is not None: - # Discard the first one, which is just the qualified name of the type - possible_module_names = split_module_names(type_name) - return possible_module_names[1:] - else: - return [] - - class TypeIndirectionVisitor(TypeVisitor[None]): """Returns all module references within a particular type.""" @@ -25,12 +15,9 @@ def __init__(self) -> None: self.modules: set[str] = set() # User to avoid infinite recursion with recursive types self.seen_types: set[types.TypeAliasType | types.Instance] = set() - # Used to avoid redundant work - self.seen_fullnames: set[str] = set() def find_modules(self, typs: Iterable[types.Type]) -> set[str]: self.modules = set() - self.seen_fullnames = set() self.seen_types = set() for typ in typs: self._visit(typ) @@ -145,11 +132,7 @@ def visit_instance(self, t: types.Instance) -> None: def visit_callable_type(self, t: types.CallableType) -> None: self._visit_type_list(t.arg_types) self._visit(t.ret_type) - if t.definition is not None: - fullname = t.definition.fullname - if fullname not in self.seen_fullnames: - self.modules.update(extract_module_names(t.definition.fullname)) - self.seen_fullnames.add(fullname) + self._visit_type_tuple(t.variables) def visit_overloaded(self, t: types.Overloaded) -> None: for item in t.items: diff --git a/mypy/nodes.py b/mypy/nodes.py index d137cd814a2b..7480745c6aa1 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -288,7 +288,7 @@ class MypyFile(SymbolNode): "path", "defs", "alias_deps", - "mod_alias_deps", + "module_refs", "is_bom", "names", "imports", @@ -312,8 +312,9 @@ class MypyFile(SymbolNode): defs: list[Statement] # Type alias dependencies as mapping from target to set of alias full names alias_deps: defaultdict[str, set[str]] - # Same as above but for coarse-grained dependencies (i.e. modules instead of full names) - mod_alias_deps: set[str] + # The set of all dependencies (suppressed or not) that this module accesses, either + # directly or indirectly. + module_refs: set[str] # Is there a UTF-8 BOM at the start? is_bom: bool names: SymbolTable @@ -354,7 +355,7 @@ def __init__( self.imports = imports self.is_bom = is_bom self.alias_deps = defaultdict(set) - self.mod_alias_deps = set() + self.module_refs = set() self.plugin_deps = {} if ignored_lines: self.ignored_lines = ignored_lines diff --git a/mypy/semanal.py b/mypy/semanal.py index 1f1e0e82c8eb..5e62d3740d0b 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -305,7 +305,14 @@ ) from mypy.types_utils import is_invalid_recursive_alias, store_argument_type from mypy.typevars import fill_typevars -from mypy.util import correct_relative_import, is_dunder, module_prefix, unmangle, unnamed_function +from mypy.util import ( + correct_relative_import, + is_dunder, + module_prefix, + split_module_names, + unmangle, + unnamed_function, +) from mypy.visitor import NodeVisitor T = TypeVar("T") @@ -2808,7 +2815,6 @@ def get_declared_metaclass( and not sym.node.alias_tvars ): target = get_proper_type(sym.node.target) - self.add_type_alias_deps({(sym.node.module, sym.node.fullname)}) if isinstance(target, Instance): metaclass_info = target.type @@ -3887,7 +3893,7 @@ def analyze_alias( declared_type_vars: TypeVarLikeList | None = None, all_declared_type_params_names: list[str] | None = None, python_3_12_type_alias: bool = False, - ) -> tuple[Type | None, list[TypeVarLikeType], set[tuple[str, str]], bool]: + ) -> tuple[Type | None, list[TypeVarLikeType], set[str], bool]: """Check if 'rvalue' is a valid type allowed for aliasing (e.g. not a type variable). If yes, return the corresponding type, a list of type variables for generic aliases, @@ -3895,7 +3901,7 @@ def analyze_alias( An example for the dependencies: A = int B = str - analyze_alias(dict[A, B])[2] == {('mod', 'mod.A'), ('mod', 'mod.B')} + analyze_alias(dict[A, B])[2] == {'__main__.A', '__main__.B'} """ dynamic = bool(self.function_stack and self.function_stack[-1].is_dynamic()) global_scope = not self.type and not self.function_stack @@ -4040,7 +4046,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: if self.is_none_alias(rvalue): res = NoneType() alias_tvars: list[TypeVarLikeType] = [] - depends_on: set[tuple[str, str]] = set() + depends_on: set[str] = set() empty_tuple_index = False else: tag = self.track_incomplete_refs() @@ -5929,6 +5935,8 @@ def visit_member_expr(self, expr: MemberExpr) -> None: if isinstance(sym.node, PlaceholderNode): self.process_placeholder(expr.name, "attribute", expr) return + if sym.node is not None: + self.record_imported_symbol(sym.node) expr.kind = sym.kind expr.fullname = sym.fullname or "" expr.node = sym.node @@ -5959,8 +5967,7 @@ def visit_member_expr(self, expr: MemberExpr) -> None: if type_info: n = type_info.names.get(expr.name) if n is not None and isinstance(n.node, (MypyFile, TypeInfo, TypeAlias)): - if not n: - return + self.record_imported_symbol(n.node) expr.kind = n.kind expr.fullname = n.fullname or "" expr.node = n.node @@ -6280,6 +6287,24 @@ def visit_class_pattern(self, p: ClassPattern) -> None: def lookup( self, name: str, ctx: Context, suppress_errors: bool = False + ) -> SymbolTableNode | None: + node = self._lookup(name, ctx, suppress_errors) + if node is not None and node.node is not None: + # This call is unfortunate from performance point of view, but + # needed for rare cases like e.g. testIncrementalChangingAlias. + self.record_imported_symbol(node.node) + return node + + def record_imported_symbol(self, node: SymbolNode) -> None: + fullname = node.fullname + if not isinstance(node, MypyFile): + while fullname not in self.modules and "." in fullname: + fullname = fullname.rsplit(".")[0] + if fullname != self.cur_mod_id and fullname not in self.cur_mod_node.module_refs: + self.cur_mod_node.module_refs.update(split_module_names(fullname)) + + def _lookup( + self, name: str, ctx: Context, suppress_errors: bool = False ) -> SymbolTableNode | None: """Look up an unqualified (no dots) name in all active namespaces. @@ -6488,6 +6513,8 @@ def lookup_qualified( self.name_not_defined(name, ctx, namespace=namespace) return None sym = nextsym + if sym is not None and sym.node is not None: + self.record_imported_symbol(sym.node) return sym def lookup_type_node(self, expr: Expression) -> SymbolTableNode | None: @@ -7526,21 +7553,18 @@ def add_plugin_dependency(self, trigger: str, target: str | None = None) -> None self.cur_mod_node.plugin_deps.setdefault(trigger, set()).add(target) def add_type_alias_deps( - self, aliases_used: Collection[tuple[str, str]], target: str | None = None + self, aliases_used: Collection[str], target: str | None = None ) -> None: """Add full names of type aliases on which the current node depends. This is used by fine-grained incremental mode to re-check the corresponding nodes. - If `target` is None, then the target node used will be the current scope. For - coarse-grained mode, add just the module names where aliases are defined. + If `target` is None, then the target node used will be the current scope. """ if not aliases_used: return if target is None: target = self.scope.current_target() - for mod, fn in aliases_used: - self.cur_mod_node.alias_deps[target].add(fn) - self.cur_mod_node.mod_alias_deps.add(mod) + self.cur_mod_node.alias_deps[target].update(aliases_used) def is_mangled_global(self, name: str) -> bool: # A global is mangled if there exists at least one renamed variant. diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 1c4d12d8ef30..658730414763 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -155,11 +155,11 @@ def analyze_type_alias( allowed_alias_tvars: list[TypeVarLikeType] | None = None, alias_type_params_names: list[str] | None = None, python_3_12_type_alias: bool = False, -) -> tuple[Type, set[tuple[str, str]]]: +) -> tuple[Type, set[str]]: """Analyze r.h.s. of a (potential) type alias definition. If `node` is valid as a type alias rvalue, return the resulting type and a set of - module and full names of type aliases it depends on (directly or indirectly). + full names of type aliases it depends on (directly or indirectly). 'node' must have been semantically analyzed. """ analyzer = TypeAnalyser( @@ -263,9 +263,8 @@ def __init__( self.options = options self.cur_mod_node = cur_mod_node self.is_typeshed_stub = is_typeshed_stub - # Names of type aliases encountered while analysing a type will be collected here - # (each tuple in the set is (module_name, fullname)). - self.aliases_used: set[tuple[str, str]] = set() + # Names of type aliases encountered while analysing a type will be collected here. + self.aliases_used: set[str] = set() self.prohibit_self_type = prohibit_self_type # Set when we analyze TypedDicts or NamedTuples, since they are special: self.prohibit_special_class_field_types = prohibit_special_class_field_types @@ -458,7 +457,7 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) if special is not None: return special if isinstance(node, TypeAlias): - self.aliases_used.add((node.module, fullname)) + self.aliases_used.add(fullname) an_args = self.anal_array( t.args, allow_param_spec=True, diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index c6081de0b371..9b79e6800648 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6411,6 +6411,37 @@ class C: ... [out2] [out3] +[case testDoubleReexportMetaUpdated] +import m +class C(metaclass=m.M): ... + +[file m.py] +from types import M + +[file types.py] +class M(type): ... +[file types.py.2] +class M: ... +[out] +[out2] +main:2: error: Metaclasses not inheriting from "type" are not supported + +[case testIncrementalOkChangeWithSave2] +import mod1 +x: int = mod1.x + +[file mod1.py] +from mod2 import x + +[file mod2.py] +x = 1 + +[file mod2.py.2] +x = "no way" +[out] +[out2] +main:2: error: Incompatible types in assignment (expression has type "str", variable has type "int") + [case testNoCrashDoubleReexportMetaEmpty] import m From 390705d13b24b9936500ef9dc11a296e7c488606 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 6 Sep 2025 17:15:57 +0100 Subject: [PATCH 09/12] A little speed-up --- mypy/semanal.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 5e62d3740d0b..bad0386065aa 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6296,8 +6296,16 @@ def lookup( return node def record_imported_symbol(self, node: SymbolNode) -> None: - fullname = node.fullname - if not isinstance(node, MypyFile): + if isinstance(node, MypyFile): + fullname = node.fullname + elif isinstance(node, TypeInfo): + fullname = node.module_name + elif isinstance(node, TypeAlias): + fullname = node.module + elif isinstance(node, (Var, FuncDef, OverloadedFuncDef)) and node.info: + fullname = node.info.module_name + else: + fullname = node.fullname.rsplit(".")[0] while fullname not in self.modules and "." in fullname: fullname = fullname.rsplit(".")[0] if fullname != self.cur_mod_id and fullname not in self.cur_mod_node.module_refs: From 5fa8b37b2806c89d2b0e37da632f910440390a0f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 6 Sep 2025 17:17:59 +0100 Subject: [PATCH 10/12] Add docstring --- mypy/semanal.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mypy/semanal.py b/mypy/semanal.py index bad0386065aa..ff77ac5e8e26 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6296,6 +6296,7 @@ def lookup( return node def record_imported_symbol(self, node: SymbolNode) -> None: + """If the symbol was not defined in current module, add its module to module_refs.""" if isinstance(node, MypyFile): fullname = node.fullname elif isinstance(node, TypeInfo): From afb06c2f17d6cab632aa4a7263bd8c90c743f92a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 6 Sep 2025 19:30:39 +0100 Subject: [PATCH 11/12] Try another micro-optimization --- mypy/semanal.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index ff77ac5e8e26..d8a960382853 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6297,6 +6297,8 @@ def lookup( def record_imported_symbol(self, node: SymbolNode) -> None: """If the symbol was not defined in current module, add its module to module_refs.""" + if not node.fullname: + return if isinstance(node, MypyFile): fullname = node.fullname elif isinstance(node, TypeInfo): @@ -6307,7 +6309,9 @@ def record_imported_symbol(self, node: SymbolNode) -> None: fullname = node.info.module_name else: fullname = node.fullname.rsplit(".")[0] - while fullname not in self.modules and "." in fullname: + if fullname == self.cur_mod_id: + return + while "." in fullname and fullname not in self.modules: fullname = fullname.rsplit(".")[0] if fullname != self.cur_mod_id and fullname not in self.cur_mod_node.module_refs: self.cur_mod_node.module_refs.update(split_module_names(fullname)) From 93aff8afd079197fa88f219fe6e03199dadfb370 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 6 Sep 2025 20:10:11 +0100 Subject: [PATCH 12/12] Try idcluding only module itself --- mypy/indirection.py | 9 ++------- mypy/semanal.py | 13 +++---------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/mypy/indirection.py b/mypy/indirection.py index 36fa305653c1..95023e303cbd 100644 --- a/mypy/indirection.py +++ b/mypy/indirection.py @@ -4,7 +4,6 @@ import mypy.types as types from mypy.types import TypeVisitor -from mypy.util import split_module_names class TypeIndirectionVisitor(TypeVisitor[None]): @@ -64,10 +63,6 @@ def _visit_type_list(self, typs: list[types.Type]) -> None: self.seen_types.add(typ) typ.accept(self) - def _visit_module_name(self, module_name: str) -> None: - if module_name not in self.modules: - self.modules.update(split_module_names(module_name)) - def visit_unbound_type(self, t: types.UnboundType) -> None: self._visit_type_tuple(t.args) @@ -118,7 +113,7 @@ def visit_instance(self, t: types.Instance) -> None: # Important optimization: instead of simply recording the definition and # recursing into bases, record the MRO and only traverse generic bases. for s in t.type.mro: - self._visit_module_name(s.module_name) + self.modules.add(s.module_name) for base in s.bases: if base.args: self._visit_type_tuple(base.args) @@ -163,6 +158,6 @@ def visit_type_alias_type(self, t: types.TypeAliasType) -> None: # Type alias is named, record its definition and continue digging into # components that constitute semantic meaning of this type: target and args. if t.alias: - self._visit_module_name(t.alias.module) + self.modules.add(t.alias.module) self._visit(t.alias.target) self._visit_type_list(t.args) diff --git a/mypy/semanal.py b/mypy/semanal.py index d8a960382853..b3fd1b98bfd2 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -305,14 +305,7 @@ ) from mypy.types_utils import is_invalid_recursive_alias, store_argument_type from mypy.typevars import fill_typevars -from mypy.util import ( - correct_relative_import, - is_dunder, - module_prefix, - split_module_names, - unmangle, - unnamed_function, -) +from mypy.util import correct_relative_import, is_dunder, module_prefix, unmangle, unnamed_function from mypy.visitor import NodeVisitor T = TypeVar("T") @@ -6313,8 +6306,8 @@ def record_imported_symbol(self, node: SymbolNode) -> None: return while "." in fullname and fullname not in self.modules: fullname = fullname.rsplit(".")[0] - if fullname != self.cur_mod_id and fullname not in self.cur_mod_node.module_refs: - self.cur_mod_node.module_refs.update(split_module_names(fullname)) + if fullname != self.cur_mod_id: + self.cur_mod_node.module_refs.add(fullname) def _lookup( self, name: str, ctx: Context, suppress_errors: bool = False