Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions code_review_graph/tools/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,29 +216,43 @@ def query_graph(
qn = node.qualified_name if node else target

if pattern == "callers_of":
seen_sources: set[str] = set()
for e in store.get_edges_by_target(qn):
if e.kind == "CALLS":
caller = store.get_node(e.source_qualified)
if caller:
results.append(node_to_dict(caller))
edges_out.append(edge_to_dict(e))
if e.source_qualified not in seen_sources:
seen_sources.add(e.source_qualified)
caller = store.get_node(e.source_qualified)
if caller:
results.append(node_to_dict(caller))
edges_out.append(edge_to_dict(e))
# Fallback: CALLS edges store unqualified target names
# (e.g. "generateTestCode") while qn is fully qualified
# (e.g. "file.ts::generateTestCode"). Search by plain name too.
if not results and node:
if node:
for e in store.search_edges_by_target_name(node.name):
caller = store.get_node(e.source_qualified)
if caller:
results.append(node_to_dict(caller))
edges_out.append(edge_to_dict(e))
if e.source_qualified not in seen_sources:
seen_sources.add(e.source_qualified)
caller = store.get_node(e.source_qualified)
if caller:
results.append(node_to_dict(caller))
edges_out.append(edge_to_dict(e))

elif pattern == "callees_of":
seen_targets: set[str] = set()
for e in store.get_edges_by_source(qn):
if e.kind == "CALLS":
callee = store.get_node(e.target_qualified)
if callee:
results.append(node_to_dict(callee))
edges_out.append(edge_to_dict(e))
if e.target_qualified not in seen_targets:
seen_targets.add(e.target_qualified)
callee = store.get_node(e.target_qualified)
if callee:
results.append(node_to_dict(callee))
elif "::" not in e.target_qualified:
results.append({
"kind": "Function",
"name": e.target_qualified,
"qualified_name": e.target_qualified,
})
edges_out.append(edge_to_dict(e))

elif pattern == "imports_of":
for e in store.get_edges_by_source(qn):
Expand Down
107 changes: 107 additions & 0 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
get_flow,
list_communities_func,
list_flows,
query_graph,
)


Expand Down Expand Up @@ -151,6 +152,112 @@ def test_search_edges_by_target_name(self):
assert edges[0].source_qualified == "/repo/main.py::process"


class TestQueryGraphCallTargetFallbacks:
"""Regression tests for mixed qualified and bare CALLS targets."""

def setup_method(self):
self.tmp_dir = tempfile.mkdtemp()
self.root = Path(self.tmp_dir).resolve()
(self.root / ".git").mkdir()
(self.root / ".code-review-graph").mkdir()

self.target_file = str(self.root / "target.m")
self.cross_file = str(self.root / "cross.m")
self.dispatch_file = str(self.root / "dispatch.m")
self.db_path = str(self.root / ".code-review-graph" / "graph.db")
self._seed_data()

def teardown_method(self):
import shutil

shutil.rmtree(self.tmp_dir, ignore_errors=True)

def _seed_data(self):
with GraphStore(self.db_path) as store:
store.upsert_node(NodeInfo(
kind="Function", name="target_func", file_path=self.target_file,
line_start=10, line_end=12, language="objc",
))
store.upsert_node(NodeInfo(
kind="Function", name="same_file_caller", file_path=self.target_file,
line_start=20, line_end=24, language="objc",
))
store.upsert_node(NodeInfo(
kind="Function", name="cross_file_caller", file_path=self.cross_file,
line_start=5, line_end=9, language="objc",
))
store.upsert_edge(EdgeInfo(
kind="CALLS",
source=f"{self.target_file}::same_file_caller",
target=f"{self.target_file}::target_func",
file_path=self.target_file,
line=22,
))
store.upsert_edge(EdgeInfo(
kind="CALLS",
source=f"{self.cross_file}::cross_file_caller",
target="target_func",
file_path=self.cross_file,
line=7,
))

store.upsert_node(NodeInfo(
kind="Function", name="dispatcher", file_path=self.dispatch_file,
line_start=1, line_end=8, language="objc",
))
store.upsert_node(NodeInfo(
kind="Function", name="resolved_helper", file_path=self.dispatch_file,
line_start=12, line_end=14, language="objc",
))
store.upsert_edge(EdgeInfo(
kind="CALLS",
source=f"{self.dispatch_file}::dispatcher",
target=f"{self.dispatch_file}::resolved_helper",
file_path=self.dispatch_file,
line=3,
))
store.upsert_edge(EdgeInfo(
kind="CALLS",
source=f"{self.dispatch_file}::dispatcher",
target="external_helper",
file_path=self.dispatch_file,
line=4,
))
store.commit()

def test_callers_of_includes_qualified_and_bare_target_callers(self):
result = query_graph(
pattern="callers_of",
target=f"{self.target_file}::target_func",
repo_root=str(self.root),
)

assert result["status"] == "ok"
names = {r["name"] for r in result["results"]}
assert names == {"same_file_caller", "cross_file_caller"}
assert len(result["results"]) == 2

edge_targets = {e["target"] for e in result["edges"]}
assert edge_targets == {f"{self.target_file}::target_func", "target_func"}

def test_callees_of_includes_resolved_and_bare_target_callees(self):
result = query_graph(
pattern="callees_of",
target=f"{self.dispatch_file}::dispatcher",
repo_root=str(self.root),
)

assert result["status"] == "ok"
names = {r["name"] for r in result["results"]}
assert names == {"resolved_helper", "external_helper"}

edge_targets = {e["target"] for e in result["edges"]}
assert edge_targets == {
f"{self.dispatch_file}::resolved_helper",
"external_helper",
}


class TestGetDocsSection:
"""Tests for the get_docs_section tool."""

Expand Down