From 2ccaa177d6afd0a275cf8d8dc93f1cb05958f8c5 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Fri, 19 Jun 2026 17:22:19 +0100 Subject: [PATCH] Fix the exportjson tool (.ff cache to .json conversion) It was broken by lazy cache deserialization, and the test didn't catch it since the fixup state was persisted within the test. Both fix the exportjson tool and make the test more realistic. --- mypy/exportjson.py | 8 ++++++-- mypy/nodes.py | 11 +++++++++++ mypy/test/testexportjson.py | 6 ++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/mypy/exportjson.py b/mypy/exportjson.py index c08f0f9f29118..7bd341401ce9d 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 f837185b858a7..e2ea348d2df11 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 58b0dea5e5a6d..294befcb3731c 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",