mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 16:09:26 +00:00
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.
This commit is contained in:
1 parent
54181b5b88
commit
4e00eaf105
2 files changed
+41
-1
No files matched your search
@@ -399,6 +399,22 @@ def extract_r(path: Path) -> dict:
|
||||
"source_file": source_file,
|
||||
"source_location": f"L{line}",
|
||||
})
|
||||
elif callee_node is not None and callee_node.type == "extract_operator":
|
||||
# `self$method()` / `private$method()`: an R6 method reaches its
|
||||
# siblings only through self/private, never as a bare name, so
|
||||
# these intra-class calls were dropped entirely. Resolve the
|
||||
# method against the enclosing class scope (walked from the caller
|
||||
# via scope_parent). super$ is left out: it dispatches to a parent
|
||||
# class this pass cannot see, so binding it locally would be wrong.
|
||||
parts = [c for c in callee_node.children if c.type == "identifier"]
|
||||
if (
|
||||
len(parts) == 2
|
||||
and _read_text(parts[0], source) in ("self", "private")
|
||||
):
|
||||
method = _read_text(parts[1], source)
|
||||
target = resolve_local(caller_id, method)
|
||||
if target is not None:
|
||||
add_edge(caller_id, target, "calls", node.start_point[0] + 1)
|
||||
elif callee_node is not None and callee_node.type == "namespace_operator":
|
||||
package_node = callee_node.child_by_field_name("lhs")
|
||||
function_node = callee_node.child_by_field_name("rhs")
|
||||
|
||||
@@ -16,7 +16,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
|
||||
from graphify.extract import extract
|
||||
from graphify.extract import extract, extract_r
|
||||
|
||||
|
||||
|
||||
@@ -111,6 +111,30 @@ def test_r_malformed_tail_comments_and_strings_do_not_create_phantoms(tmp_path):
|
||||
assert labels.isdisjoint({'ghost()', 'hidden()'})
|
||||
|
||||
|
||||
def test_r6_self_and_private_method_calls_resolve(tmp_path):
|
||||
"""R6 methods reach their siblings through `self$` / `private$`, never as a
|
||||
bare name. Those intra-class calls were dropped because walk_calls only
|
||||
handled a bare identifier callee."""
|
||||
source = tmp_path / "counter.R"
|
||||
source.write_text(
|
||||
'Counter <- R6Class("Counter",\n'
|
||||
" public = list(\n"
|
||||
" increment = function() self$report(),\n"
|
||||
" report = function() print(1)\n"
|
||||
" ),\n"
|
||||
" private = list(\n"
|
||||
" log = function() private$fmt(),\n"
|
||||
" fmt = function() 2\n"
|
||||
" )\n"
|
||||
")\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = extract_r(source)
|
||||
calls = _edge_labels(result, "calls")
|
||||
assert ("increment()", "report()") in calls, "self$ call dropped"
|
||||
assert ("log()", "fmt()") in calls, "private$ call dropped"
|
||||
|
||||
|
||||
def test_r_missing_parser_reports_install_hint(tmp_path, monkeypatch, capsys):
|
||||
source = tmp_path / "missing.R"
|
||||
source.write_text('run <- function() 1\n', encoding="utf-8")
|
||||
|
||||
Reference in new issue
Block a user