query_graph(pattern="callers_of", ...) returns incomplete results for Objective-C and C codebases. When a function has both same-file callers (edges with fully-qualified target_qualified) and cross-file callers (edges with bare target_qualified), only the same-file callers are returned. The cross-file callers are silently dropped.
Root Cause
In code_review_graph/tools/query.py, the callers_of branch (line ~226):
if pattern == "callers_of":
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))
# Fallback: search by plain name for unqualified targets
if not results and node: # <--- BUG
for e in store.search_edges_by_target_name(node.name):
...
The fallback search_edges_by_target_name only executes when results is empty. But in ObjC/C, the parser (_resolve_call_target in parser.py) generates:
- Fully-qualified targets (e.g.
file.m::ClassName.methodName) when the callee is defined in the same file (present in defined_names)
- Bare targets (e.g.
methodName) when the callee is defined in a different file (not in defined_names, not in import_map)
Since get_edges_by_target(qn) matches the qualified edges first, results becomes non-empty, and the fallback that would catch the bare-target cross-file edges is skipped.
Impact
- 87.2% of CALLS edges in the affected codebase have bare (unqualified) targets
- Any
callers_of query that has ≥1 same-file caller will miss all cross-file callers
- This is especially severe for ObjC where Category methods are spread across many files —
[self methodDefinedInAnotherCategory] always produces bare targets
Reproduction
Given this ObjC codebase structure:
// ClawGame.m (Category 1 - defines the method)
@implementation KSKTVRoomViewController (ClawGame)
- (void)startLoadClawGameViewInfo { ... } // line 31: DEFINITION
- (void)handleClawGameMessage:(KSIMMessage *)message {
[self startLoadClawGameViewInfo]; // line 99: same-file call → qualified target
}
@end
// RoomControl.m (Category 2 - calls the method)
@implementation KSKTVRoomViewController (RoomControl)
- (void)handleDelayAfterEnterRoom {
[self startLoadClawGameViewInfo]; // line 3009: cross-file call → bare target
}
@end
After code-review-graph build:
# Query returns only 1 caller, should return 2
result = query_graph(pattern="callers_of", target="startLoadClawGameViewInfo")
assert len(result["results"]) == 1 # FAILS: expected 2
The database correctly contains both edges:
| edge_id |
target_qualified |
file |
line |
| 109958 |
...ClawGame.m::KSKTVRoomViewController.startLoadClawGameViewInfo |
ClawGame.m |
99 |
| 136509 |
startLoadClawGameViewInfo |
RoomControl.m |
3009 |
But query_graph only returns edge 109958.
Proposed Fix
Change the fallback condition from if not results to always execute and deduplicate:
if pattern == "callers_of":
seen_sources = 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))
seen_sources.add(e.source_qualified)
edges_out.append(edge_to_dict(e))
# Always search by plain name for unqualified targets, deduplicating
if node:
for e in store.search_edges_by_target_name(node.name):
if e.source_qualified not in seen_sources:
caller = store.get_node(e.source_qualified)
if caller:
results.append(node_to_dict(caller))
seen_sources.add(e.source_qualified)
edges_out.append(edge_to_dict(e))
The same fix should be applied to callees_of (which has the same structural issue when the callee's node isn't in the graph).
Environment
code-review-graph version: 2.3.3
- Language: Objective-C (also affects C based on the same
_resolve_call_target logic)
- OS: macOS (arm64)
- Python: 3.14
query_graph(pattern="callers_of", ...)returns incomplete results for Objective-C and C codebases. When a function has both same-file callers (edges with fully-qualifiedtarget_qualified) and cross-file callers (edges with baretarget_qualified), only the same-file callers are returned. The cross-file callers are silently dropped.Root Cause
In
code_review_graph/tools/query.py, thecallers_ofbranch (line ~226):The fallback
search_edges_by_target_nameonly executes whenresultsis empty. But in ObjC/C, the parser (_resolve_call_targetinparser.py) generates:file.m::ClassName.methodName) when the callee is defined in the same file (present indefined_names)methodName) when the callee is defined in a different file (not indefined_names, not inimport_map)Since
get_edges_by_target(qn)matches the qualified edges first,resultsbecomes non-empty, and the fallback that would catch the bare-target cross-file edges is skipped.Impact
callers_ofquery that has ≥1 same-file caller will miss all cross-file callers[self methodDefinedInAnotherCategory]always produces bare targetsReproduction
Given this ObjC codebase structure:
After
code-review-graph build:The database correctly contains both edges:
...ClawGame.m::KSKTVRoomViewController.startLoadClawGameViewInfostartLoadClawGameViewInfoBut
query_graphonly returns edge 109958.Proposed Fix
Change the fallback condition from
if not resultsto always execute and deduplicate:The same fix should be applied to
callees_of(which has the same structural issue when the callee's node isn't in the graph).Environment
code-review-graphversion: 2.3.3_resolve_call_targetlogic)