Skip to content

Commit d89fad0

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 1a3a83a commit d89fad0

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
@@ -770,6 +770,16 @@ def parse_bytes(self, path: Path, source: bytes) -> tuple[list[NodeInfo], list[E
770770
tree.root_node, language, file_path_str, edges, import_map,
771771
)
772772

773+
# Enrich: detect function/class references passed as call arguments
774+
self._enrich_func_ref_args(
775+
tree.root_node, language, file_path_str, edges, defined_names,
776+
)
777+
778+
# Enrich: detect function/class references returned or assigned
779+
self._enrich_func_ref_returns(
780+
tree.root_node, language, file_path_str, edges, defined_names,
781+
)
782+
773783
# Resolve bare call targets to qualified names using same-file definitions
774784
edges = self._resolve_call_targets(nodes, edges, file_path_str, import_map)
775785

@@ -3692,6 +3702,236 @@ def _extract_dunder_all(root) -> Optional[set[str]]:
36923702
return set()
36933703
return None
36943704

3705+
# ------------------------------------------------------------------
3706+
# Function-reference-as-argument enrichment
3707+
# ------------------------------------------------------------------
3708+
3709+
def _enrich_func_ref_args(
3710+
self,
3711+
root,
3712+
language: str,
3713+
file_path: str,
3714+
edges: list[EdgeInfo],
3715+
defined_names: set[str],
3716+
) -> None:
3717+
"""Detect function/class names passed as arguments to calls."""
3718+
if not defined_names:
3719+
return
3720+
arg_list_types = {
3721+
"argument_list", "arguments", "value_arguments",
3722+
"actual_parameters", "template_argument_list",
3723+
}
3724+
ident_types = {"identifier", "simple_identifier"}
3725+
kw_types = {"keyword_argument", "value_argument", "named_argument"}
3726+
3727+
self._walk_func_ref_args(
3728+
root, language, file_path, edges, defined_names,
3729+
arg_list_types, ident_types, kw_types,
3730+
enclosing_func=None, enclosing_class=None,
3731+
)
3732+
3733+
def _walk_func_ref_args(
3734+
self,
3735+
node,
3736+
language: str,
3737+
file_path: str,
3738+
edges: list[EdgeInfo],
3739+
defined_names: set[str],
3740+
arg_list_types: set[str],
3741+
ident_types: set[str],
3742+
kw_types: set[str],
3743+
enclosing_func: Optional[str],
3744+
enclosing_class: Optional[str],
3745+
_depth: int = 0,
3746+
) -> None:
3747+
if _depth > 50:
3748+
return
3749+
func_types = set(_FUNCTION_TYPES.get(language, []))
3750+
3751+
for child in node.children:
3752+
if child.type in func_types:
3753+
fname = self._get_name(child, language, "function")
3754+
if fname:
3755+
defined_names.add(fname)
3756+
self._walk_func_ref_args(
3757+
child, language, file_path, edges, defined_names,
3758+
arg_list_types, ident_types, kw_types,
3759+
enclosing_func=fname, enclosing_class=enclosing_class,
3760+
_depth=_depth + 1,
3761+
)
3762+
continue
3763+
3764+
if child.type in arg_list_types:
3765+
for arg in child.children:
3766+
ref_name = None
3767+
line = arg.start_point[0] + 1
3768+
if arg.type in ident_types:
3769+
ref_name = arg.text.decode("utf-8", errors="replace")
3770+
elif arg.type in kw_types:
3771+
for sub in arg.children:
3772+
if sub.type in ident_types:
3773+
ref_name = sub.text.decode("utf-8", errors="replace")
3774+
elif arg.type == "callable_reference":
3775+
for sub in arg.children:
3776+
if sub.type in ident_types:
3777+
ref_name = sub.text.decode("utf-8", errors="replace")
3778+
3779+
if ref_name and ref_name in defined_names:
3780+
source = (
3781+
self._qualify(enclosing_func, file_path, enclosing_class)
3782+
if enclosing_func else file_path
3783+
)
3784+
edges.append(EdgeInfo(
3785+
kind="CALLS",
3786+
source=source,
3787+
target=ref_name,
3788+
file_path=file_path,
3789+
line=line,
3790+
))
3791+
continue
3792+
3793+
if child.type == "jsx_expression":
3794+
for sub in child.children:
3795+
if sub.type in ident_types:
3796+
ref_name = sub.text.decode("utf-8", errors="replace")
3797+
if ref_name in defined_names:
3798+
source = (
3799+
self._qualify(
3800+
enclosing_func, file_path, enclosing_class,
3801+
)
3802+
if enclosing_func else file_path
3803+
)
3804+
edges.append(EdgeInfo(
3805+
kind="CALLS",
3806+
source=source,
3807+
target=ref_name,
3808+
file_path=file_path,
3809+
line=sub.start_point[0] + 1,
3810+
))
3811+
continue
3812+
3813+
self._walk_func_ref_args(
3814+
child, language, file_path, edges, defined_names,
3815+
arg_list_types, ident_types, kw_types,
3816+
enclosing_func=enclosing_func,
3817+
enclosing_class=enclosing_class,
3818+
_depth=_depth + 1,
3819+
)
3820+
3821+
# ------------------------------------------------------------------
3822+
# Function-reference in return/assignment enrichment
3823+
# ------------------------------------------------------------------
3824+
3825+
def _enrich_func_ref_returns(
3826+
self,
3827+
root,
3828+
language: str,
3829+
file_path: str,
3830+
edges: list[EdgeInfo],
3831+
defined_names: set[str],
3832+
) -> None:
3833+
"""Detect function/class names used as values in return and assignment."""
3834+
if not defined_names:
3835+
return
3836+
ident_types = {"identifier", "simple_identifier"}
3837+
return_types = {"return_statement"}
3838+
assign_types = {
3839+
"assignment", "variable_declarator",
3840+
"assignment_expression", "augmented_assignment",
3841+
}
3842+
self._walk_func_ref_returns(
3843+
root, language, file_path, edges, defined_names,
3844+
ident_types, return_types, assign_types,
3845+
enclosing_func=None, enclosing_class=None,
3846+
)
3847+
3848+
def _walk_func_ref_returns(
3849+
self,
3850+
node,
3851+
language: str,
3852+
file_path: str,
3853+
edges: list[EdgeInfo],
3854+
defined_names: set[str],
3855+
ident_types: set[str],
3856+
return_types: set[str],
3857+
assign_types: set[str],
3858+
enclosing_func: Optional[str],
3859+
enclosing_class: Optional[str],
3860+
_depth: int = 0,
3861+
) -> None:
3862+
if _depth > 50:
3863+
return
3864+
func_types = set(_FUNCTION_TYPES.get(language, []))
3865+
3866+
for child in node.children:
3867+
if child.type in func_types:
3868+
fname = self._get_name(child, language, "function")
3869+
if fname:
3870+
self._walk_func_ref_returns(
3871+
child, language, file_path, edges, defined_names,
3872+
ident_types, return_types, assign_types,
3873+
enclosing_func=fname, enclosing_class=enclosing_class,
3874+
_depth=_depth + 1,
3875+
)
3876+
continue
3877+
3878+
if child.type in return_types:
3879+
for sub in child.children:
3880+
if sub.type in ident_types:
3881+
ref_name = sub.text.decode("utf-8", errors="replace")
3882+
if ref_name in defined_names:
3883+
source = (
3884+
self._qualify(
3885+
enclosing_func, file_path, enclosing_class,
3886+
)
3887+
if enclosing_func else file_path
3888+
)
3889+
edges.append(EdgeInfo(
3890+
kind="CALLS",
3891+
source=source,
3892+
target=ref_name,
3893+
file_path=file_path,
3894+
line=sub.start_point[0] + 1,
3895+
))
3896+
continue
3897+
3898+
if child.type in assign_types:
3899+
last_ident = None
3900+
for sub in child.children:
3901+
if sub.type in ident_types:
3902+
last_ident = sub
3903+
if last_ident:
3904+
ref_name = last_ident.text.decode("utf-8", errors="replace")
3905+
if ref_name in defined_names:
3906+
first_ident = None
3907+
for sub in child.children:
3908+
if sub.type in ident_types:
3909+
first_ident = sub
3910+
break
3911+
if first_ident and first_ident != last_ident:
3912+
source = (
3913+
self._qualify(
3914+
enclosing_func, file_path, enclosing_class,
3915+
)
3916+
if enclosing_func else file_path
3917+
)
3918+
edges.append(EdgeInfo(
3919+
kind="CALLS",
3920+
source=source,
3921+
target=ref_name,
3922+
file_path=file_path,
3923+
line=last_ident.start_point[0] + 1,
3924+
))
3925+
continue
3926+
3927+
self._walk_func_ref_returns(
3928+
child, language, file_path, edges, defined_names,
3929+
ident_types, return_types, assign_types,
3930+
enclosing_func=enclosing_func,
3931+
enclosing_class=enclosing_class,
3932+
_depth=_depth + 1,
3933+
)
3934+
36953935
# ------------------------------------------------------------------
36963936
# Typed-variable call enrichment
36973937
# ------------------------------------------------------------------

0 commit comments

Comments
 (0)