Skip to content

Commit aa2584c

Browse files
rajatnagda45safishamsi
authored andcommitted
fix(r): resolve R6 self$ and private$ intra-class method calls
An R6 method reaches a sibling only through self$method() or private$method(), never as a bare name, but walk_calls only handled a bare identifier callee. So every intra-class call inside an R6 class was dropped, leaving methods that clearly call each other disconnected in the graph. Handle an extract_operator callee whose receiver is self or private: resolve the method against the enclosing class scope, which is already reachable from the caller via scope_parent. super$ is deliberately excluded — it dispatches to a parent class this single-file pass cannot see, so a local binding would be wrong. Added a regression test covering both self$ and private$ calls. (cherry picked from commit 4e00eaf)
1 parent a4c6323 commit aa2584c

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

‎graphify/extractors/r.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,22 @@ def walk_calls(node: Node, caller_id: str) -> None:
409409
"source_file": source_file,
410410
"source_location": f"L{line}",
411411
})
412+
elif callee_node is not None and callee_node.type == "extract_operator":
413+
# `self$method()` / `private$method()`: an R6 method reaches its
414+
# siblings only through self/private, never as a bare name, so
415+
# these intra-class calls were dropped entirely. Resolve the
416+
# method against the enclosing class scope (walked from the caller
417+
# via scope_parent). super$ is left out: it dispatches to a parent
418+
# class this pass cannot see, so binding it locally would be wrong.
419+
parts = [c for c in callee_node.children if c.type == "identifier"]
420+
if (
421+
len(parts) == 2
422+
and _read_text(parts[0], source) in ("self", "private")
423+
):
424+
method = _read_text(parts[1], source)
425+
target = resolve_local(caller_id, method)
426+
if target is not None:
427+
add_edge(caller_id, target, "calls", node.start_point[0] + 1)
412428
elif callee_node is not None and callee_node.type == "namespace_operator":
413429
package_node = callee_node.child_by_field_name("lhs")
414430
function_node = callee_node.child_by_field_name("rhs")

‎tests/test_r_extractor.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,30 @@ def test_r_namespaced_setrefclass_is_recognised(tmp_path):
148148
assert ("Acc", "add()") in _edge_labels(result, "method")
149149

150150

151+
def test_r6_self_and_private_method_calls_resolve(tmp_path):
152+
"""R6 methods reach their siblings through `self$` / `private$`, never as a
153+
bare name. Those intra-class calls were dropped because walk_calls only
154+
handled a bare identifier callee."""
155+
source = tmp_path / "counter.R"
156+
source.write_text(
157+
'Counter <- R6Class("Counter",\n'
158+
" public = list(\n"
159+
" increment = function() self$report(),\n"
160+
" report = function() print(1)\n"
161+
" ),\n"
162+
" private = list(\n"
163+
" log = function() private$fmt(),\n"
164+
" fmt = function() 2\n"
165+
" )\n"
166+
")\n",
167+
encoding="utf-8",
168+
)
169+
result = extract_r(source)
170+
calls = _edge_labels(result, "calls")
171+
assert ("increment()", "report()") in calls, "self$ call dropped"
172+
assert ("log()", "fmt()") in calls, "private$ call dropped"
173+
174+
151175
def test_r_missing_parser_reports_install_hint(tmp_path, monkeypatch, capsys):
152176
source = tmp_path / "missing.R"
153177
source.write_text('run <- function() 1\n', encoding="utf-8")

0 commit comments

Comments
 (0)