diff --git a/mypy/build.py b/mypy/build.py index 2271365f3aff..84dbf2b2df88 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 from mypy.partially_defined import PossiblyUndefinedVariableVisitor from mypy.semanal import SemanticAnalyzer from mypy.semanal_pass1 import SemanticAnalyzerPreAnalysis @@ -1765,26 +1765,24 @@ 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 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 ------------- -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. @@ -2409,21 +2407,15 @@ 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()) - for _, sym, _ in self.tree.local_definitions(): - if sym.type is not None: - all_types.append(sym.type) - 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) + self._patch_indirect_dependencies( + # 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: dump_type_stats( @@ -2452,7 +2444,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() diff --git a/mypy/checker.py b/mypy/checker.py index 5843148d4b4e..96b55f321a73 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/fixup.py b/mypy/fixup.py index 260c0f84cf1b..d0205f64b720 100644 --- a/mypy/fixup.py +++ b/mypy/fixup.py @@ -441,4 +441,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 4e566194632b..95023e303cbd 100644 --- a/mypy/indirection.py +++ b/mypy/indirection.py @@ -4,17 +4,6 @@ import mypy.types as types from mypy.types import TypeVisitor -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]): @@ -23,49 +12,57 @@ 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() + # User to avoid infinite recursion with recursive types + self.seen_types: set[types.TypeAliasType | types.Instance] = 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. - 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: - 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) @@ -105,27 +102,36 @@ 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) + self.modules.add(s.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) 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: - 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,4 +155,9 @@ def visit_type_type(self, t: types.TypeType) -> None: self._visit(t.item) def visit_type_alias_type(self, t: types.TypeAliasType) -> None: - self._visit(types.get_proper_type(t)) + # 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.modules.add(t.alias.module) + self._visit(t.alias.target) + self._visit_type_list(t.args) diff --git a/mypy/nodes.py b/mypy/nodes.py index 9cfc61c80b3e..7480745c6aa1 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -288,6 +288,7 @@ class MypyFile(SymbolNode): "path", "defs", "alias_deps", + "module_refs", "is_bom", "names", "imports", @@ -311,6 +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]] + # 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 @@ -351,6 +355,7 @@ def __init__( self.imports = imports self.is_bom = is_bom self.alias_deps = defaultdict(set) + self.module_refs = set() self.plugin_deps = {} if ignored_lines: self.ignored_lines = ignored_lines @@ -4121,7 +4126,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 +4141,7 @@ def f(x: B[T]) -> T: ... # without T, Any would be used here __slots__ = ( "target", "_fullname", + "module", "alias_tvars", "no_args", "normalized", @@ -4150,6 +4157,7 @@ def __init__( self, target: mypy.types.Type, fullname: str, + module: str, line: int, column: int, *, @@ -4160,6 +4168,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 +4203,7 @@ def from_tuple_type(cls, info: TypeInfo) -> TypeAlias: ) ), info.fullname, + info.module_name, info.line, info.column, ) @@ -4215,6 +4225,7 @@ def from_typeddict_type(cls, info: TypeInfo) -> TypeAlias: ) ), info.fullname, + info.module_name, info.line, info.column, ) @@ -4238,6 +4249,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 +4264,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 +4276,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 +4288,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 +4300,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..b3fd1b98bfd2 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, @@ -3885,16 +3886,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[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] == {'__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 @@ -3906,10 +3906,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)): @@ -3945,9 +3944,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,11 +4040,10 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: res = NoneType() alias_tvars: list[TypeVarLikeType] = [] depends_on: set[str] = set() - qualified_tvars: list[str] = [] 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, @@ -4070,12 +4067,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. @@ -4106,6 +4097,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, @@ -5577,7 +5569,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, @@ -5606,12 +5598,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 ) @@ -5627,6 +5613,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, @@ -5941,6 +5928,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 @@ -5971,8 +5960,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 @@ -6292,6 +6280,37 @@ 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: + """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): + 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] + 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: + self.cur_mod_node.module_refs.add(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. @@ -6500,6 +6519,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: @@ -7546,8 +7567,6 @@ def add_type_alias_deps( If `target` is None, then the target node used will be the current scope. """ 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() diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 9d4445a1f7e7..076d95e2baf9 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/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 d1155f54a75d..06f228721a86 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -6171,6 +6171,118 @@ class Base: [out2] main:5: error: Call to abstract method "meth" of "Base" with trivial body via super() is unsafe +[case testLiteralCoarseGrainedChainedAliases] +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 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] +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 pb1, pb2 +C = pb2.C + +[file pb1.py] +class C: ... +[file pb2.py] +class C: ... +[out] +[out2] +[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 @@ -6203,6 +6315,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 @@ -6235,6 +6379,69 @@ class C: ... [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 +[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 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 @@ -6267,6 +6474,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