mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 15:40:05 +00:00
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)
107 lines
3.8 KiB
Python
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)
|