diff --git a/mypy/exportjson.py b/mypy/exportjson.py index c08f0f9f2911..7bd341401ce9 100644 --- a/mypy/exportjson.py +++ b/mypy/exportjson.py @@ -128,8 +128,12 @@ def convert_symbol_table_node(self: SymbolTableNode, cfg: Config) -> Json: data["plugin_generated"] = True if self.cross_ref: data["cross_ref"] = self.cross_ref - elif self.node is not None: - data["node"] = convert_symbol_node(self.node, cfg) + else: + # Read the raw node without cross-reference fixup, since exportjson reads + # cache files in isolation and no node fixer is available. + node = self.read_node_no_fixup() + if node is not None: + data["node"] = convert_symbol_node(node, cfg) return data diff --git a/mypy/nodes.py b/mypy/nodes.py index f837185b858a..e2ea348d2df1 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -4952,6 +4952,17 @@ def node(self) -> SymbolNode | None: self.unfixed = False return self._node + def read_node_no_fixup(self) -> SymbolNode | None: + """Return the deserialized node without performing cross-reference fixup. + + This is intended for introspection tools (such as mypy.exportjson) that read + cache files in isolation, where no node fixer is available. + """ + if self._node is None and self._node_bytes: + self._node = read_symbol(ReadBuffer(self._node_bytes), self._node_tag) + self._node_bytes = b"" + return self._node + def copy(self) -> SymbolTableNode: new = SymbolTableNode( self.kind, self._node, self.module_public, self.implicit, self.module_hidden diff --git a/mypy/test/testexportjson.py b/mypy/test/testexportjson.py index 58b0dea5e5a6..294befcb3731 100644 --- a/mypy/test/testexportjson.py +++ b/mypy/test/testexportjson.py @@ -11,6 +11,7 @@ from mypy.errors import CompileError from mypy.exportjson import convert_binary_cache_meta_to_json, convert_binary_cache_to_json from mypy.modulefinder import BuildSource +from mypy.modules_state import modules_state from mypy.options import Options from mypy.test.config import test_temp_dir from mypy.test.data import DataDrivenTestCase, DataSuite @@ -44,6 +45,11 @@ def run_case(self, testcase: DataDrivenTestCase) -> None: major, minor = sys.version_info[:2] cache_dir = os.path.join(".mypy_cache", f"{major}.{minor}") + # Reset the global fixup state, since the exportjson tool + # reads cache files in isolation (no node fixer available). + modules_state.node_fixer = None + modules_state.modules = {} + for module in result.files: if module in ( "builtins",