Skip to content
Merged
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
167 changes: 167 additions & 0 deletions scripts/parsers/js_backend_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,24 @@
keep_alive.append(child)
stack.append((child, depth + 1))

# Issue #222: collect arrow functions / function expressions
# assigned to object-literal properties (e.g.
# ``const svc = { list: (ctx) => { ... } }``) and register
# them as ``<varName>.<key>`` nodes. Without this pass the
# arrow function is invisible as a node — ``trace``/``impact``/
# ``search --mode symbol`` cannot resolve it by name, even
# though ``ref_count`` of callees is correct (calls inside
# the arrow body are extracted by the module-level pass below
# and attributed to ``<module>``).
#
# This pass must run BEFORE _find_module_level_calls so the
# module-level pass can skip pair subtrees that are now
# registered as method nodes (see _find_module_level_calls
# pair-skip branch).
self._collect_object_literal_method_calls(
root, source, file_path, nodes, edges, keep_alive,
)

# Issue #210: module-top-level pass.
# The main walk above only extracts calls via _parse_and_collect_calls,
# which is invoked on declaration nodes (function_declaration,
Expand Down Expand Up @@ -297,6 +315,142 @@
decl_info.pop("body_node", None)
return decl_info

def _collect_object_literal_method_calls(

Check failure on line 318 in scripts/parsers/js_backend_parser.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 91 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Wolfvin_CodeLens&issues=AZ9UUqdlWh4_BOS_YToq&open=AZ9UUqdlWh4_BOS_YToq&pullRequest=225
self,
root: Node,
source: bytes,
file_path: str,
nodes: List[Dict],
edges: List[Dict],
keep_alive: List[Node],
) -> None:
"""Find arrow functions / function expressions assigned to
object-literal properties and register them as function nodes,
then collect call edges from their bodies.

Issue #222: ``const svc = { list: (ctx) => { ... } }`` — the
``variable_declarator`` value is an ``object`` node (not
``arrow_function``), so ``_parse_variable_declarator`` returns
None and the arrow function is invisible as a node. Calls
inside the arrow body are still extracted by the module-level
pass (issue #210) and attributed to ``<module>``, so
``ref_count`` of callees is correct — but ``trace``/``impact``/
``search --mode symbol`` cannot resolve the method by name.

This pass walks the AST and, for every ``variable_declarator``
whose value is an ``object`` literal, iterates the object's
``pair`` children. Each pair whose value is an ``arrow_function``
or ``function_expression`` is:

1. Registered as a function node named ``<varName>.<key>``
(mirroring the Pinia store convention).
2. Has its body calls collected and attributed to the new
node id, so the module-level pass can skip the pair
subtree without losing the calls.

Naming:
- ``const svc = { list: (ctx) => {} }`` → node ``svc.list``
- Object literal not assigned to a variable (e.g. returned
from a function or passed as a call argument): skipped —
there is no stable name to use, and the module-level pass
already extracts body calls for those.

Uses the same iterative DFS + ``keep_alive`` pattern as the
main walk (issue #116/#163) so tree-sitter Node references
don't dangle mid-walk.
"""
MAX_DEPTH = 200
stack: List[Tuple[Node, int]] = [(root, 0)]
while stack:
node, depth = stack.pop()
if depth > MAX_DEPTH:
continue

if node.type == 'variable_declarator':
# Find the identifier (var name) and the object value.
var_name = None
object_node = None
for child in node.children:
keep_alive.append(child)
if child.type == 'identifier' and var_name is None:
var_name = self.get_text(child, source)
elif child.type == 'object':
object_node = child

if var_name and object_node:
# Walk the object's pair children and register
# arrow/function values as method declarations.
for pair in object_node.children:
keep_alive.append(pair)
if pair.type != 'pair':
continue
key_node = None
value_node = None
for pc in pair.children:
keep_alive.append(pc)
if pc.type in ('property_identifier', 'identifier') and key_node is None:
key_node = pc
elif pc.type in ('arrow_function', 'function_expression'):
value_node = pc
if key_node is None or value_node is None:
continue

key_name = self.get_text(key_node, source)
fn_name = f"{var_name}.{key_name}"

# Check async
is_async = False
for vc in value_node.children:
keep_alive.append(vc)
if vc.type == 'async':
is_async = True
break

line = self.get_line(pair)
node_id = f"{file_path}:{line}"

# Find the body
body_node = None
for vc in value_node.children:
keep_alive.append(vc)
if vc.type in ('statement_block', 'expression'):
body_node = vc
break

node_dict = {
"id": node_id,
"fn": fn_name,
"file": file_path,
"line": line,
"async": is_async,
"node_type": "object_method",
}
nodes.append(node_dict)

if body_node is not None:
fn_calls = self._find_calls_in_scope(
body_node, source, file_path,
)
for call_info in fn_calls:
edge = {
"from": node_id,
"to_fn": call_info["fn_name"],
"via_self": call_info.get("via_self", False)
}
if call_info.get("is_ipc_call"):
edge["is_ipc_call"] = True
edges.append(edge)
# Don't recurse into the variable_declarator —
# pairs are handled above, and non-arrow values
# (e.g. numbers, strings) have no calls to extract.
continue

if depth + 1 <= MAX_DEPTH:
children = node.children
for child in reversed(children):
keep_alive.append(child)
stack.append((child, depth + 1))

def _find_function_declarations(self, root: Node, source: bytes,
file_path: str) -> List[Dict]:
"""Find all function declarations in the AST."""
Expand Down Expand Up @@ -619,6 +773,19 @@
if _is_registered_value(child):
skip_subtree = True
break
elif node.type == 'pair':
# Issue #222: skip pair nodes whose value is an
# arrow_function or function_expression — these are
# now registered as ``<varName>.<key>`` method nodes
# by :meth:`_collect_object_literal_method_calls`, so
# their body calls are already collected. Without
# this skip, calls inside the arrow body would be
# double-counted (once as ``<varName>.<key>`` → callee,
# once as ``<module>`` → callee).
for child in node.children:
if child.type in ('arrow_function', 'function_expression'):
skip_subtree = True
break

if skip_subtree:
continue
Expand Down
141 changes: 141 additions & 0 deletions scripts/parsers/ts_backend_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@

Returns:
{"nodes": [...], "edges": [...]}

Issue #222: arrow functions assigned to object-literal properties
(e.g. ``const svc = { list: (ctx) => { ... } }``) are now registered
as function nodes named ``<varName>.<key>`` so that:
- ``trace``/``impact`` can resolve them as call targets
- ``search --mode symbol`` finds them by name
- calls inside their bodies are attributed to the named method
rather than to the synthetic ``<module>`` caller

Previously these arrow functions were invisible as nodes — calls
inside their bodies were extracted by the module-level pass
(issue #210) and attributed to ``<module>``, so ``ref_count`` was
correct but ``trace``/``impact``/``search`` could not see the
method itself.
"""
source = content.encode('utf-8')
tree = self.parse(source)
Expand All @@ -64,6 +78,14 @@
# First pass: find all function declarations
fn_declarations = self._find_function_declarations(tree, source, file_path)

# Issue #222: also find arrow functions / function expressions
# assigned to object-literal properties (e.g.
# ``const svc = { list: (ctx) => { ... } }``) and register them
# as ``<varName>.<key>`` nodes so trace/impact/search can see them.
fn_declarations.extend(
self._find_object_literal_method_decls(tree, source, file_path)
)

# Build a map of line -> function for scope resolution
fn_scope_map = self._build_scope_map(fn_declarations)

Expand Down Expand Up @@ -375,6 +397,113 @@
self.walk_tree(body_node, source, visit)
return calls

def _find_object_literal_method_decls(self, root: Node, source: bytes,

Check failure on line 400 in scripts/parsers/ts_backend_parser.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 44 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Wolfvin_CodeLens&issues=AZ9UUqbjWh4_BOS_YTop&open=AZ9UUqbjWh4_BOS_YTop&pullRequest=225
file_path: str) -> List[Dict]:
"""Find arrow functions / function expressions assigned to
object-literal properties and register them as function nodes.

Issue #222: ``const svc = { list: (ctx) => { ... } }`` — the
``variable_declarator`` value is an ``object`` node (not
``arrow_function``), so ``_parse_variable_declarator`` returns
None and the arrow function is invisible as a node.

This pass walks the AST and, for every ``variable_declarator``
whose value is an ``object`` literal, iterates the object's
``pair`` children. Each pair whose value is an ``arrow_function``
or ``function_expression`` is registered as a function node named
``<varName>.<key>`` — mirroring the Pinia store convention so
downstream tools (``trace``, ``impact``, ``search --mode symbol``)
can resolve the method by name.

Returns a list of declaration dicts in the same shape as
:meth:`_find_function_declarations`, so the caller can merge
them into ``fn_declarations`` and let the per-function pass
collect body calls uniformly.

Naming:
- ``const svc = { list: (ctx) => {} }`` → node ``svc.list``
- Nested object: ``const svc = { api: { list: () => {} } }``
only the outermost var name is used → ``svc.list`` (the
inner ``api`` key is not part of the name; this keeps
naming simple and matches how Pinia store methods are
named today).
- Object literal not assigned to a variable (e.g. returned
from a function or passed as a call argument): skipped —
there is no stable name to use, and the module-level pass
(issue #210) already extracts body calls for those.
"""
declarations: List[Dict] = []

def visit(node: Node, _, depth):
if node.type != 'variable_declarator':
return True
# Find the identifier (var name) and the object value.
var_name = None
object_node = None
for child in node.children:
if child.type == 'identifier' and var_name is None:
var_name = self.get_text(child, source)
elif child.type == 'object':
object_node = child
if not var_name or not object_node:
return True

# Walk the object's pair children and register arrow/function
# values as method declarations.
for pair in object_node.children:
if pair.type != 'pair':
continue
key_node = None
value_node = None
for pc in pair.children:
if pc.type in ('property_identifier', 'identifier') and key_node is None:
key_node = pc
elif pc.type in ('arrow_function', 'function_expression'):
value_node = pc
if key_node is None or value_node is None:
continue

key_name = self.get_text(key_node, source)
fn_name = f"{var_name}.{key_name}"

# Check async
is_async = False
for vc in value_node.children:
if vc.type == 'async':
is_async = True
break

line = self.get_line(pair)
node_id = f"{file_path}:{line}"

# Find the body
body_node = None
for vc in value_node.children:
if vc.type in ('statement_block', 'expression'):
body_node = vc
break

declarations.append({
"node": {
"id": node_id,
"fn": fn_name,
"file": file_path,
"line": line,
"async": is_async,
"node_type": "object_method",
},
"body_node": body_node,
"scope_start": pair.start_point.row,
"scope_end": pair.end_point.row,
})
# Don't recurse into the object — pairs are handled above,
# and nested non-arrow values (e.g. numbers, strings) have
# no calls to extract.
return False

self.walk_tree(root, source, visit)
return declarations

def _find_module_level_calls(self, root: Node, source: bytes,
file_path: str) -> List[Dict]:
"""Find call sites that live outside any registered function declaration.
Expand Down Expand Up @@ -439,6 +568,18 @@
for child in node.children:
if _is_registered_value(child):
return False
# Issue #222: skip pair nodes whose value is an arrow_function
# or function_expression — these are now registered as
# ``<varName>.<key>`` method nodes by
# :meth:`_find_object_literal_method_decls`, so their body
# calls are already collected by the per-function pass.
# Without this skip, calls inside the arrow body would be
# double-counted (once as ``<varName>.<key>`` → callee, once
# as ``<module>`` → callee).
if node.type == 'pair':
for child in node.children:
if child.type in ('arrow_function', 'function_expression'):
return False

if node.type == 'call_expression':
call_info = self._parse_call(node, source)
Expand Down
Loading
Loading