Skip to content

Commit 106f984

Browse files
committed
feat(schema): L1 body call nodes (callee null) from call sites
1 parent 239b621 commit 106f984

3 files changed

Lines changed: 44 additions & 0 deletions

File tree

codeanalyzer/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
model_validate_json,
1919
)
2020
from codeanalyzer.schema.assign_ids import assign_ids
21+
from codeanalyzer.schema.l1_body import populate_l1_body
2122
from codeanalyzer.schema.py_schema import PyCallEdge
2223
from codeanalyzer.semantic_analysis.call_graph import (
2324
filter_external_edges,
@@ -457,6 +458,7 @@ def analyze(self) -> PyApplication:
457458
)
458459

459460
assign_ids(app, self.options.app_name or self.project_dir.name)
461+
populate_l1_body(app)
460462

461463
# L3/L4 dataflow emission rebuilt on the v2 tree in Stage 3+
462464

codeanalyzer/schema/l1_body.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""L1 body population: materialize `call` nodes from existing call sites.
2+
`callee` is left None here — the sanctioned null→id refinement happens at L2."""
3+
from __future__ import annotations
4+
from codeanalyzer.schema.py_schema import PyApplication, PyClass, PyCallable, BodyNode, Span, byte_offsets
5+
6+
def _do_callable(source: str, c: PyCallable) -> None:
7+
for cs in c.call_sites or []:
8+
key = f"{cs.start_line}:{cs.start_column}"
9+
span = Span(start=(cs.start_line, cs.start_column),
10+
end=(cs.end_line, cs.end_column),
11+
bytes=byte_offsets(source, cs.start_line, cs.start_column, cs.end_line, cs.end_column)) if source else None
12+
c.body[key] = BodyNode(kind="call", span=span, callee=None)
13+
for ic in (c.inner_callables or {}).values():
14+
_do_callable(source, ic)
15+
for icl in (c.inner_classes or {}).values():
16+
_do_class(source, icl)
17+
18+
def _do_class(source: str, cl: PyClass) -> None:
19+
for m in (cl.methods or {}).values():
20+
_do_callable(source, m)
21+
for ic in (cl.inner_classes or {}).values():
22+
_do_class(source, ic)
23+
24+
def populate_l1_body(app: PyApplication) -> None:
25+
for mod in app.symbol_table.values():
26+
for fn in (mod.functions or {}).values():
27+
_do_callable(mod.source, fn)
28+
for cl in (mod.classes or {}).values():
29+
_do_class(mod.source, cl)

test/test_v2_l1_body.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from codeanalyzer.schema.l1_body import populate_l1_body
2+
from codeanalyzer.schema.py_schema import PyApplication, PyModule, PyCallable, PyCallsite
3+
4+
def test_l1_body_has_call_nodes_with_null_callee():
5+
cs = PyCallsite(method_name="g", start_line=2, start_column=4, end_line=2,
6+
end_column=7, callee_signature="m.g")
7+
fn = PyCallable(name="f", path="m.py", signature="m.f", call_sites=[cs])
8+
mod = PyModule(file_path="m.py", module_name="m", functions={"f": fn})
9+
app = PyApplication(symbol_table={"m.py": mod})
10+
populate_l1_body(app)
11+
node = fn.body["2:4"]
12+
assert node.kind == "call"
13+
assert node.callee is None # L1: unresolved; backfilled at L2

0 commit comments

Comments
 (0)