Files
graphify/tests/test_lua_method_calls.py
T
rajatnagda45 1e11bd6942 fix(lua): resolve colon-method calls (obj:method())
A colon call `obj:method()` parses as a method_index_expression whose
callee is in the `method` field and whose receiver is in the `table`
field, but _LUA_CONFIG read the absent `name` field, so the callee was
never captured and every colon call was dropped from the call graph.

Point the accessor at the right fields and, when the receiver is `self`,
rewrite the bare method name to its enclosing-table label (Animal:method)
so it resolves like any other colon-method definition. An arbitrary-object
receiver (other:m()) is left unqualified and deferred so it cannot bind to
an unrelated same-named bare function.

Fixes #3991

(cherry picked from commit 000b0775c0)
2026-10-02 16:50:45 +01:00

107 lines
3.8 KiB
Python

from pathlib import Path
from graphify.extract import extract
def _write(path: Path, text: str) -> Path:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
return path
def _call_edges(path: Path, base: Path) -> set[tuple[str, str]]:
result = extract([path], root=base, cache_root=base, parallel=False)
label = {node["id"]: node["label"] for node in result["nodes"]}
return {
(label.get(edge["source"], edge["source"]), label.get(edge["target"], edge["target"]))
for edge in result["edges"]
if edge["relation"] in ("calls", "indirect_call")
}
def test_lua_self_colon_call_resolves_to_sibling_method(tmp_path):
"""`self:other()` inside a colon method is sugar for a call to a sibling
method on the same table. The receiver `self` is untyped on its own, but the
enclosing method's table (`Animal`) is known, so the call must resolve to
`Animal:describe`. Before the fix the colon call was dropped entirely: the
`method_index_expression` callee lives in the `method` field, but the Lua
config read the (absent) `name` field, so no call edge was emitted (#3991)."""
src = _write(
tmp_path / "animal.lua",
"local Animal = {}\n"
"function Animal:speak()\n"
" return self:describe()\n"
"end\n"
"function Animal:describe()\n"
" return self.name\n"
"end\n",
)
assert ("Animal:speak()", "Animal:describe()") in _call_edges(src, tmp_path)
def test_lua_self_colon_call_resolves_dot_defined_sibling(tmp_path):
"""A sibling declared with dot syntax (`function Animal.helper()`) is the
same method; `self:helper()` must still resolve to it (#3991)."""
src = _write(
tmp_path / "animal.lua",
"local Animal = {}\n"
"function Animal.helper()\n"
" return 1\n"
"end\n"
"function Animal:run()\n"
" return self:helper()\n"
"end\n",
)
assert ("Animal:run()", "Animal.helper()") in _call_edges(src, tmp_path)
def test_lua_self_colon_call_does_not_bind_to_unrelated_bare_function(tmp_path):
"""Fail-closed: when the table has no matching sibling, `self:process()` must
NOT bind to an unrelated top-level function named `process`. Resolution stays
scoped to the enclosing table (#3991)."""
src = _write(
tmp_path / "animal.lua",
"function process()\n"
" return 1\n"
"end\n"
"local Animal = {}\n"
"function Animal:run()\n"
" return self:process()\n"
"end\n",
)
assert ("Animal:run()", "process()") not in _call_edges(src, tmp_path)
def test_lua_colon_call_on_unknown_receiver_is_fail_closed(tmp_path):
"""A colon call on an arbitrary receiver (`obj:process()`) has an unknown
type, so it must NOT bind by bare name to an unrelated top-level
`process` — only the literal `self` receiver is resolved (#3991)."""
src = _write(
tmp_path / "animal.lua",
"function process()\n"
" return 1\n"
"end\n"
"local Animal = {}\n"
"function Animal:run()\n"
" local obj = make()\n"
" return obj:process()\n"
"end\n",
)
assert ("Animal:run()", "process()") not in _call_edges(src, tmp_path)
def test_lua_dot_and_plain_calls_still_resolve(tmp_path):
"""Positive control: the dot-call (`M.helper()`) and plain-call (`helper()`)
paths that already worked must keep working (#3991)."""
src = _write(
tmp_path / "mod.lua",
"local M = {}\n"
"function M.helper()\n"
" return 1\n"
"end\n"
"function M.main()\n"
" return M.helper()\n"
"end\n",
)
assert ("M.main()", "M.helper()") in _call_edges(src, tmp_path)