diff --git a/mypy/semanal.py b/mypy/semanal.py index fe6bd71c1ab9..85690330f5c8 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -6751,8 +6751,59 @@ def create_getattr_var( return v return None + def _lookup_fully_qualified( + self, fullname: str, *, allow_missing: bool + ) -> SymbolTableNode | None: + """Implementation detail shared by fully qualified lookup helpers. + + This is not part of the public or plugin APIs. It only exists to centralize + the traversal logic so the public helpers remain thin wrappers without + changing any semantic rules governed by the analyzer. + """ + if "." not in fullname: + raise ValueError( + f"_lookup_fully_qualified requires a qualified name with at least one dot, " + f"got: {fullname!r}" + ) + + module: str | None = None + filenode: MypyFile | None = None + parts = fullname.split(".") + names: list[str] = [] + + while parts: + candidate = ".".join(parts) + filenode = self.modules.get(candidate) + if filenode is not None: + module = candidate + break + names.append(parts.pop()) + + if filenode is None or module is None or not names: + return None + + names.reverse() + result = filenode.names.get(names[0]) + + if result is None and self.is_incomplete_namespace(module): + # TODO: More explicit handling of incomplete refs? + self.record_incomplete_ref() + # When the namespace is incomplete and we don't have a result, + # return None immediately. If allow_missing=False, the caller + # will assert this is not None (preserving original behavior). + # If allow_missing=True, returning None is the expected outcome. + return None + + for part in names[1:]: + if result is not None and isinstance(result.node, TypeInfo): + result = result.node.names.get(part) + else: + return None + + return result + def lookup_fully_qualified(self, fullname: str) -> SymbolTableNode: - ret = self.lookup_fully_qualified_or_none(fullname) + ret = self._lookup_fully_qualified(fullname, allow_missing=False) assert ret is not None, fullname return ret @@ -6765,47 +6816,7 @@ def lookup_fully_qualified_or_none(self, fullname: str) -> SymbolTableNode | Non Note that this can't be used for names nested in class namespaces. """ - # TODO: unify/clean-up/simplify lookup methods, see #4157. - module, name = fullname.rsplit(".", maxsplit=1) - - if module in self.modules: - # If the module exists, look up the name in the module. - # This is the common case. - filenode = self.modules[module] - result = filenode.names.get(name) - if result is None and self.is_incomplete_namespace(module): - # TODO: More explicit handling of incomplete refs? - self.record_incomplete_ref() - return result - else: - # Else, try to find the longest prefix of the module name that is in the modules dictionary. - splitted_modules = fullname.split(".") - names = [] - - while splitted_modules and ".".join(splitted_modules) not in self.modules: - names.append(splitted_modules.pop()) - - if not splitted_modules or not names: - # If no module or name is found, return None. - return None - - # Reverse the names list to get the correct order of names. - names.reverse() - - module = ".".join(splitted_modules) - filenode = self.modules[module] - result = filenode.names.get(names[0]) - - if result is None and self.is_incomplete_namespace(module): - # TODO: More explicit handling of incomplete refs? - self.record_incomplete_ref() - - for part in names[1:]: - if result is not None and isinstance(result.node, TypeInfo): - result = result.node.names.get(part) - else: - return None - return result + return self._lookup_fully_qualified(fullname, allow_missing=True) def object_type(self) -> Instance: if self._object_type is None: @@ -6822,33 +6833,67 @@ def function_type(self) -> Instance: self._function_type = self.named_type("builtins.function") return self._function_type - def named_type(self, fullname: str, args: list[Type] | None = None) -> Instance: - sym = self.lookup_fully_qualified(fullname) - assert sym, "Internal error: attempted to construct unknown type" - node = sym.node - assert isinstance(node, TypeInfo), node - if args: - # TODO: assert len(args) == len(node.defn.type_vars) - return Instance(node, args) - return Instance(node, [AnyType(TypeOfAny.special_form)] * len(node.defn.type_vars)) + def _build_named_instance( + self, + fullname: str, + args: list[Type] | None, + *, + allow_missing: bool, + any_flavor: int, + unwrap_alias: bool, + ) -> Instance | None: + """Internal helper to construct Instances for fully-qualified names. + + This exists solely to remove duplication between the public helpers. It + doesn't change user-visible behavior and is not part of the public or + plugin APIs. + """ + if allow_missing: + sym = self.lookup_fully_qualified_or_none(fullname) + if not sym or isinstance(sym.node, PlaceholderNode): + return None + else: + sym = self.lookup_fully_qualified(fullname) - def named_type_or_none(self, fullname: str, args: list[Type] | None = None) -> Instance | None: - sym = self.lookup_fully_qualified_or_none(fullname) - if not sym or isinstance(sym.node, PlaceholderNode): - return None node = sym.node - if isinstance(node, TypeAlias): + if unwrap_alias and isinstance(node, TypeAlias): assert isinstance(node.target, Instance) # type: ignore[misc] node = node.target.type assert isinstance(node, TypeInfo), node - if args is not None: - # TODO: assert len(args) == len(node.defn.type_vars) - return Instance(node, args) - return Instance(node, [AnyType(TypeOfAny.unannotated)] * len(node.defn.type_vars)) + + inst_args = args + if inst_args is None: + inst_args = [AnyType(any_flavor)] * len(node.defn.type_vars) + # TODO: assert len(inst_args) == len(node.defn.type_vars) + return Instance(node, inst_args) + + def named_type(self, fullname: str, args: list[Type] | None = None) -> Instance: + inst = self._build_named_instance( + fullname, + args, + allow_missing=False, + any_flavor=TypeOfAny.special_form, + unwrap_alias=False, + ) + assert inst is not None, "Internal error: attempted to construct unknown type" + return inst + + def named_type_or_none(self, fullname: str, args: list[Type] | None = None) -> Instance | None: + return self._build_named_instance( + fullname, args, allow_missing=True, any_flavor=TypeOfAny.unannotated, unwrap_alias=True + ) def builtin_type(self, fully_qualified_name: str) -> Instance: """Legacy function -- use named_type() instead.""" - return self.named_type(fully_qualified_name) + inst = self._build_named_instance( + fully_qualified_name, + None, + allow_missing=False, + any_flavor=TypeOfAny.special_form, + unwrap_alias=False, + ) + assert inst is not None + return inst def lookup_current_scope(self, name: str) -> SymbolTableNode | None: if self.locals[-1] is not None: