Serialise *tree* to AST JSON, reusing encoded subtrees from *old_chunks* for any chunk whose content hash AND start_line are both unchanged. Skips encode_node() for every unchanged top-level function/class — typically 80-100 % of body nodes when only a few lines change. Return
(
tree: ast.Module,
source: str,
old_chunks: Dict[str, AstChunk],
)
| 127 | |
| 128 | |
| 129 | def _build_ast_json_and_chunks( |
| 130 | tree: ast.Module, |
| 131 | source: str, |
| 132 | old_chunks: Dict[str, AstChunk], |
| 133 | ) -> Tuple[str, Dict[str, AstChunk]]: |
| 134 | """ |
| 135 | Serialise *tree* to AST JSON, reusing encoded subtrees from *old_chunks* |
| 136 | for any chunk whose content hash AND start_line are both unchanged. |
| 137 | |
| 138 | Skips encode_node() for every unchanged top-level function/class — |
| 139 | typically 80-100 % of body nodes when only a few lines change. |
| 140 | |
| 141 | Returns (full_ast_json, new_chunks_dict). |
| 142 | """ |
| 143 | lines = source.splitlines(keepends=True) |
| 144 | seen: Dict[str, int] = {} |
| 145 | new_chunks: Dict[str, AstChunk] = {} |
| 146 | body_parts: List[str] = [] |
| 147 | |
| 148 | for node in tree.body: |
| 149 | cid = _make_chunk_id(node, seen) |
| 150 | src = _source_slice(lines, node) |
| 151 | end = getattr(node, "end_lineno", node.lineno) |
| 152 | new_hash = hashlib.sha256(src.encode()).hexdigest() |
| 153 | |
| 154 | old = old_chunks.get(cid) |
| 155 | reuse = ( |
| 156 | old is not None |
| 157 | and old.content_hash == new_hash |
| 158 | and old.start_line == node.lineno |
| 159 | ) |
| 160 | |
| 161 | if reuse: |
| 162 | assert old is not None # type narrowing |
| 163 | node_json = zlib.decompress(old.ast_json_z).decode() |
| 164 | chunk_z = old.ast_json_z |
| 165 | else: |
| 166 | node_json = encode_node(node) |
| 167 | chunk_z = zlib.compress(node_json.encode(), _ZLIB_LEVEL) |
| 168 | |
| 169 | new_chunks[cid] = AstChunk( |
| 170 | chunk_id=cid, |
| 171 | start_line=node.lineno, |
| 172 | end_line=end, |
| 173 | content_hash=new_hash, |
| 174 | ast_json_z=chunk_z, |
| 175 | ) |
| 176 | body_parts.append(node_json) |
| 177 | |
| 178 | type_ignore_parts = [encode_node(ti) for ti in tree.type_ignores] |
| 179 | full_json = _assemble_module_json(body_parts, type_ignore_parts) |
| 180 | return full_json, new_chunks |
| 181 | |
| 182 | |
| 183 | # ── Disk serialization — JSON + base64, no executable deserialization ───────── |