Skip to content

Commit ec8a806

Browse files
committed
feat(schema): store module source once + byte-offset spans, drop per-node code
1 parent 6bbadc6 commit ec8a806

2 files changed

Lines changed: 42 additions & 8 deletions

File tree

codeanalyzer/syntactic_analysis/symbol_table_builder.py

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
PyModule,
2222
PySymbol,
2323
PyVariableDeclaration,
24+
Span,
25+
byte_offsets,
2426
)
2527

2628

@@ -123,11 +125,12 @@ def build_pymodule_from_file(self, py_file: Path) -> PyModule:
123125
PyModule.builder()
124126
.file_path(str(py_file))
125127
.module_name(py_file.stem)
128+
.source(source)
126129
.comments(self._pycomments(module, source))
127130
.imports(self._imports(module))
128131
.variables(self._module_variables(module, script))
129-
.classes(self._add_class(module, script))
130-
.functions(self._callables(module, script))
132+
.classes(self._add_class(module, script, source))
133+
.functions(self._callables(module, script, source))
131134
.content_hash(content_hash)
132135
.last_modified(last_modified)
133136
.file_size(file_size)
@@ -183,7 +186,7 @@ def _imports(self, module: ast.Module) -> List[PyImport]:
183186

184187
return imports
185188

186-
def _add_class(self, node: AST, script: Script, prefix: str = "") -> Dict[str, PyClass]:
189+
def _add_class(self, node: AST, script: Script, source: str, prefix: str = "") -> Dict[str, PyClass]:
187190
classes: Dict[str, PyClass] = {}
188191

189192
for child in ast.iter_child_nodes(node):
@@ -194,6 +197,14 @@ def _add_class(self, node: AST, script: Script, prefix: str = "") -> Dict[str, P
194197
start_line = child.lineno
195198
end_line = getattr(child, "end_lineno", start_line + len(child.body))
196199
code = ast.unparse(child).strip()
200+
span = Span(
201+
start=(child.lineno, child.col_offset),
202+
end=(getattr(child, "end_lineno", child.lineno),
203+
getattr(child, "end_col_offset", child.col_offset)),
204+
bytes=byte_offsets(source, child.lineno, child.col_offset,
205+
getattr(child, "end_lineno", child.lineno),
206+
getattr(child, "end_col_offset", child.col_offset)),
207+
)
197208

198209
# Try resolving full signature with Jedi
199210
if prefix:
@@ -211,6 +222,7 @@ def _add_class(self, node: AST, script: Script, prefix: str = "") -> Dict[str, P
211222
PyClass.builder()
212223
.name(class_name)
213224
.signature(signature)
225+
.span(span)
214226
.start_line(start_line)
215227
.end_line(end_line)
216228
.comments(self._pycomments(child, code))
@@ -219,9 +231,9 @@ def _add_class(self, node: AST, script: Script, prefix: str = "") -> Dict[str, P
219231
for base in child.bases
220232
if isinstance(base, ast.expr)
221233
])
222-
.methods(self._callables(child, script, prefix=signature)) # Pass class signature as prefix
234+
.methods(self._callables(child, script, source, prefix=signature)) # Pass class signature as prefix
223235
.attributes(self._class_attributes(child, script))
224-
.inner_classes(self._add_class(child, script, prefix=signature)) # Pass class signature as prefix
236+
.inner_classes(self._add_class(child, script, source, prefix=signature)) # Pass class signature as prefix
225237
.build()
226238
)
227239

@@ -230,7 +242,7 @@ def _add_class(self, node: AST, script: Script, prefix: str = "") -> Dict[str, P
230242
return classes
231243

232244

233-
def _callables(self, node: AST, script: Script, prefix: str = "") -> Dict[str, PyCallable]:
245+
def _callables(self, node: AST, script: Script, source: str, prefix: str = "") -> Dict[str, PyCallable]:
234246
callables: Dict[str, PyCallable] = {}
235247

236248
for child in ast.iter_child_nodes(node):
@@ -239,6 +251,14 @@ def _callables(self, node: AST, script: Script, prefix: str = "") -> Dict[str, P
239251
start_line = child.lineno
240252
end_line = getattr(child, "end_lineno", start_line + len(child.body))
241253
code = ast.unparse(child).strip()
254+
span = Span(
255+
start=(child.lineno, child.col_offset),
256+
end=(getattr(child, "end_lineno", child.lineno),
257+
getattr(child, "end_col_offset", child.col_offset)),
258+
bytes=byte_offsets(source, child.lineno, child.col_offset,
259+
getattr(child, "end_lineno", child.lineno),
260+
getattr(child, "end_col_offset", child.col_offset)),
261+
)
242262
decorators = [ast.unparse(d) for d in child.decorator_list]
243263

244264
if prefix:
@@ -264,6 +284,7 @@ def _callables(self, node: AST, script: Script, prefix: str = "") -> Dict[str, P
264284
.name(method_name) # Use the actual method name, not the full signature
265285
.path(str(script.path))
266286
.signature(signature) # Use the full signature here
287+
.span(span)
267288
.decorators(decorators)
268289
.start_line(start_line)
269290
.end_line(end_line)
@@ -278,8 +299,8 @@ def _callables(self, node: AST, script: Script, prefix: str = "") -> Dict[str, P
278299
if child.returns else self._infer_type(script, child.lineno, child.col_offset)
279300
)
280301
.comments(self._pycomments(child, code))
281-
.inner_callables(self._callables(child, script, signature)) # Pass current signature as prefix
282-
.inner_classes(self._add_class(child, script, signature)) # Pass current signature as prefix
302+
.inner_callables(self._callables(child, script, source, signature)) # Pass current signature as prefix
303+
.inner_classes(self._add_class(child, script, source, signature)) # Pass current signature as prefix
283304
.build()
284305
)
285306

test/test_v2_source_spans.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
from pathlib import Path
2+
13
from codeanalyzer.schema.py_schema import byte_offsets
4+
from codeanalyzer.syntactic_analysis.symbol_table_builder import SymbolTableBuilder
25

36

47
def test_byte_offsets_slice_source_exactly():
@@ -12,3 +15,13 @@ def test_byte_offsets_multibyte_safe():
1215
source = "x = 'é'\ny = 2\n" # 'é' is 2 bytes in utf-8
1316
lo, hi = byte_offsets(source, 2, 0, 2, 5)
1417
assert source.encode("utf-8")[lo:hi].decode("utf-8") == "y = 2"
18+
19+
20+
def test_module_stores_source_and_callable_span_slices_it(tmp_path: Path):
21+
f = tmp_path / "m.py"
22+
f.write_text("def f(a):\n return a\n", encoding="utf-8")
23+
mod = SymbolTableBuilder(tmp_path, None).build_pymodule_from_file(f)
24+
assert mod.source == "def f(a):\n return a\n"
25+
fn = next(iter(mod.functions.values()))
26+
lo, hi = fn.span.bytes
27+
assert mod.source.encode("utf-8")[lo:hi].decode("utf-8").startswith("def f(a):")

0 commit comments

Comments
 (0)