Skip to content

Commit 078b0a7

Browse files
committed
feat(parser): enrich CALLS via function-reference args and returns
Add two enrichers that catch function/class references that aren't invoked at the reference site but still establish a call relationship: `_enrich_func_ref_args`: scans call argument lists for identifiers that match a locally-defined function or class name, plus JSX expression attributes (`onClick={handler}`) and Kotlin callable references (`::agentThread`). `_enrich_func_ref_returns`: scans return statements and assignments for identifiers that match a defined name. Skips self-referential assignments (`x = x`). These prevent dead-code false positives for callbacks, thread targets, and registered handlers (e.g. `Thread(target=worker)`, `HTTPServer(addr, Handler)`, `return myCallback`).
1 parent be5c6c0 commit 078b0a7

1 file changed

Lines changed: 240 additions & 0 deletions

File tree

code_review_graph/parser.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,16 @@ def parse_bytes(self, path: Path, source: bytes) -> tuple[list[NodeInfo], list[E
10231023
tree.root_node, language, file_path_str, edges, import_map,
10241024
)
10251025

1026+
# Enrich: detect function/class references passed as call arguments
1027+
self._enrich_func_ref_args(
1028+
tree.root_node, language, file_path_str, edges, defined_names,
1029+
)
1030+
1031+
# Enrich: detect function/class references returned or assigned
1032+
self._enrich_func_ref_returns(
1033+
tree.root_node, language, file_path_str, edges, defined_names,
1034+
)
1035+
10261036
# Resolve bare call targets to qualified names using same-file definitions
10271037
edges = self._resolve_call_targets(nodes, edges, file_path_str, import_map)
10281038

@@ -5354,6 +5364,236 @@ def _extract_dunder_all(root) -> Optional[set[str]]:
53545364
return set()
53555365
return None
53565366

5367+
# ------------------------------------------------------------------
5368+
# Function-reference-as-argument enrichment
5369+
# ------------------------------------------------------------------
5370+
5371+
def _enrich_func_ref_args(
5372+
self,
5373+
root,
5374+
language: str,
5375+
file_path: str,
5376+
edges: list[EdgeInfo],
5377+
defined_names: set[str],
5378+
) -> None:
5379+
"""Detect function/class names passed as arguments to calls."""
5380+
if not defined_names:
5381+
return
5382+
arg_list_types = {
5383+
"argument_list", "arguments", "value_arguments",
5384+
"actual_parameters", "template_argument_list",
5385+
}
5386+
ident_types = {"identifier", "simple_identifier"}
5387+
kw_types = {"keyword_argument", "value_argument", "named_argument"}
5388+
5389+
self._walk_func_ref_args(
5390+
root, language, file_path, edges, defined_names,
5391+
arg_list_types, ident_types, kw_types,
5392+
enclosing_func=None, enclosing_class=None,
5393+
)
5394+
5395+
def _walk_func_ref_args(
5396+
self,
5397+
node,
5398+
language: str,
5399+
file_path: str,
5400+
edges: list[EdgeInfo],
5401+
defined_names: set[str],
5402+
arg_list_types: set[str],
5403+
ident_types: set[str],
5404+
kw_types: set[str],
5405+
enclosing_func: Optional[str],
5406+
enclosing_class: Optional[str],
5407+
_depth: int = 0,
5408+
) -> None:
5409+
if _depth > 50:
5410+
return
5411+
func_types = set(_FUNCTION_TYPES.get(language, []))
5412+
5413+
for child in node.children:
5414+
if child.type in func_types:
5415+
fname = self._get_name(child, language, "function")
5416+
if fname:
5417+
defined_names.add(fname)
5418+
self._walk_func_ref_args(
5419+
child, language, file_path, edges, defined_names,
5420+
arg_list_types, ident_types, kw_types,
5421+
enclosing_func=fname, enclosing_class=enclosing_class,
5422+
_depth=_depth + 1,
5423+
)
5424+
continue
5425+
5426+
if child.type in arg_list_types:
5427+
for arg in child.children:
5428+
ref_name = None
5429+
line = arg.start_point[0] + 1
5430+
if arg.type in ident_types:
5431+
ref_name = arg.text.decode("utf-8", errors="replace")
5432+
elif arg.type in kw_types:
5433+
for sub in arg.children:
5434+
if sub.type in ident_types:
5435+
ref_name = sub.text.decode("utf-8", errors="replace")
5436+
elif arg.type == "callable_reference":
5437+
for sub in arg.children:
5438+
if sub.type in ident_types:
5439+
ref_name = sub.text.decode("utf-8", errors="replace")
5440+
5441+
if ref_name and ref_name in defined_names:
5442+
source = (
5443+
self._qualify(enclosing_func, file_path, enclosing_class)
5444+
if enclosing_func else file_path
5445+
)
5446+
edges.append(EdgeInfo(
5447+
kind="CALLS",
5448+
source=source,
5449+
target=ref_name,
5450+
file_path=file_path,
5451+
line=line,
5452+
))
5453+
continue
5454+
5455+
if child.type == "jsx_expression":
5456+
for sub in child.children:
5457+
if sub.type in ident_types:
5458+
ref_name = sub.text.decode("utf-8", errors="replace")
5459+
if ref_name in defined_names:
5460+
source = (
5461+
self._qualify(
5462+
enclosing_func, file_path, enclosing_class,
5463+
)
5464+
if enclosing_func else file_path
5465+
)
5466+
edges.append(EdgeInfo(
5467+
kind="CALLS",
5468+
source=source,
5469+
target=ref_name,
5470+
file_path=file_path,
5471+
line=sub.start_point[0] + 1,
5472+
))
5473+
continue
5474+
5475+
self._walk_func_ref_args(
5476+
child, language, file_path, edges, defined_names,
5477+
arg_list_types, ident_types, kw_types,
5478+
enclosing_func=enclosing_func,
5479+
enclosing_class=enclosing_class,
5480+
_depth=_depth + 1,
5481+
)
5482+
5483+
# ------------------------------------------------------------------
5484+
# Function-reference in return/assignment enrichment
5485+
# ------------------------------------------------------------------
5486+
5487+
def _enrich_func_ref_returns(
5488+
self,
5489+
root,
5490+
language: str,
5491+
file_path: str,
5492+
edges: list[EdgeInfo],
5493+
defined_names: set[str],
5494+
) -> None:
5495+
"""Detect function/class names used as values in return and assignment."""
5496+
if not defined_names:
5497+
return
5498+
ident_types = {"identifier", "simple_identifier"}
5499+
return_types = {"return_statement"}
5500+
assign_types = {
5501+
"assignment", "variable_declarator",
5502+
"assignment_expression", "augmented_assignment",
5503+
}
5504+
self._walk_func_ref_returns(
5505+
root, language, file_path, edges, defined_names,
5506+
ident_types, return_types, assign_types,
5507+
enclosing_func=None, enclosing_class=None,
5508+
)
5509+
5510+
def _walk_func_ref_returns(
5511+
self,
5512+
node,
5513+
language: str,
5514+
file_path: str,
5515+
edges: list[EdgeInfo],
5516+
defined_names: set[str],
5517+
ident_types: set[str],
5518+
return_types: set[str],
5519+
assign_types: set[str],
5520+
enclosing_func: Optional[str],
5521+
enclosing_class: Optional[str],
5522+
_depth: int = 0,
5523+
) -> None:
5524+
if _depth > 50:
5525+
return
5526+
func_types = set(_FUNCTION_TYPES.get(language, []))
5527+
5528+
for child in node.children:
5529+
if child.type in func_types:
5530+
fname = self._get_name(child, language, "function")
5531+
if fname:
5532+
self._walk_func_ref_returns(
5533+
child, language, file_path, edges, defined_names,
5534+
ident_types, return_types, assign_types,
5535+
enclosing_func=fname, enclosing_class=enclosing_class,
5536+
_depth=_depth + 1,
5537+
)
5538+
continue
5539+
5540+
if child.type in return_types:
5541+
for sub in child.children:
5542+
if sub.type in ident_types:
5543+
ref_name = sub.text.decode("utf-8", errors="replace")
5544+
if ref_name in defined_names:
5545+
source = (
5546+
self._qualify(
5547+
enclosing_func, file_path, enclosing_class,
5548+
)
5549+
if enclosing_func else file_path
5550+
)
5551+
edges.append(EdgeInfo(
5552+
kind="CALLS",
5553+
source=source,
5554+
target=ref_name,
5555+
file_path=file_path,
5556+
line=sub.start_point[0] + 1,
5557+
))
5558+
continue
5559+
5560+
if child.type in assign_types:
5561+
last_ident = None
5562+
for sub in child.children:
5563+
if sub.type in ident_types:
5564+
last_ident = sub
5565+
if last_ident:
5566+
ref_name = last_ident.text.decode("utf-8", errors="replace")
5567+
if ref_name in defined_names:
5568+
first_ident = None
5569+
for sub in child.children:
5570+
if sub.type in ident_types:
5571+
first_ident = sub
5572+
break
5573+
if first_ident and first_ident != last_ident:
5574+
source = (
5575+
self._qualify(
5576+
enclosing_func, file_path, enclosing_class,
5577+
)
5578+
if enclosing_func else file_path
5579+
)
5580+
edges.append(EdgeInfo(
5581+
kind="CALLS",
5582+
source=source,
5583+
target=ref_name,
5584+
file_path=file_path,
5585+
line=last_ident.start_point[0] + 1,
5586+
))
5587+
continue
5588+
5589+
self._walk_func_ref_returns(
5590+
child, language, file_path, edges, defined_names,
5591+
ident_types, return_types, assign_types,
5592+
enclosing_func=enclosing_func,
5593+
enclosing_class=enclosing_class,
5594+
_depth=_depth + 1,
5595+
)
5596+
53575597
# ------------------------------------------------------------------
53585598
# Typed-variable call enrichment
53595599
# ------------------------------------------------------------------

0 commit comments

Comments
 (0)