Repository navigation
Description
What happened
In Lua, a function expression assigned to a table field or a table-constructor key produces no node. Only function M.name() / function M:name() / local function name() are extracted.
The parser clearly walks into those function bodies — locals declared inside them do get variable nodes — but the enclosing function itself is never registered. Its outgoing calls are then attributed to the file node, so callers and impact report a confident-looking answer that silently omits real call sites.
Minimal reproduction
Two files, nothing else. codegraph 1.5.0, Windows.
util.lua
util = {}
function util.helper()
return 1
end
return utilhandlers.lua
local M = {}
function M.namedFn() -- extracted
return util.helper()
end
M.assignedFn = function() -- NOT extracted
return util.helper()
end
M.callbacks = {
onStart = function() -- NOT extracted
return util.helper()
end,
}
return Mcodegraph init . # 5 nodes, 6 edgesActual
Nodes: file handlers.lua, variable M, method namedFn, file util.lua, method helper.
assignedFn and onStart are absent.
$ codegraph callers helper
Callers of "helper" (2):
method namedFn
handlers.lua:5
file handlers.lua
handlers.lua:1
Three call sites exist; two of them collapse into a single file handlers.lua:1 entry that names neither function and points at line 1. There is no uncertainty marker, so the answer reads as complete.
Expected
assignedFn and onStart extracted as function/method nodes named after the assignment target (M.assignedFn, M.callbacks.onStart), with their own outgoing calls edges — the same treatment function M.namedFn() already gets.
Why it matters
This is the dominant way real Lua codebases attach behaviour. Measured on an indexed 2,107-file Lua game client (25,623 nodes):
- Every UI handler is invisible. Widgets bind callbacks as
tbWnd.tbOnClick = { btnUse = function(self) ... end }; toolwide,togShowInHud,HOTKEY_QINGKUNG,fnCheck,fnMoveOnAttack→ 0 nodes. - Dispatch tables are invisible — e.g. a
tbKeyDownFun = { HOTKEY_RIDE = function() ... end, ... }hotkey table yields onevariablenode and nothing for the handlers. - Node kinds across the whole index:
method19,600 ·variable3,894 ·file2,107 ·function20 ·import2. Thefunctioncount is 20 because onlylocal functionreaches that kind.
The impact is not just missing nodes: every call made from inside these functions is re-attributed to the file, which both hides callers and inflates the file node's apparent fan-out.
Related
- TypeScript:
export const alias = fnbreaks caller/impact traversal — callers/impact of the implementation omit every consumer calling through the alias (2-file repro) #1482 (TypeScriptexport const alias = fn) — same family, but there the binding is indexed and only the composition step is missing. In Lua there is no node at all. - Luau/Lua: PascalCase method calls (obj:Method()) self-match the local-var type-annotation pattern, breaking receiver resolution on ambiguous names #1124 / Extend local-variable method-call resolution to Lua, Luau, R, and Pascal/Delphi #1112 covered Lua call resolution; this report is about extraction.
Variant worth folding in
local f = function() ... end does produce a node, but with kind = variable, and no calls edge ever targets it — so calls through f() are lost the same way.
Activity
fix(lua): index assignment-style function definitions (#1650) (#1778)
colbymchenry commentedon Sep 8, 2026
Fixed by #1778 / #1650
This is the same extraction gap as #1650. The landed fix in #1778 (
38cb0bd5, merge8c047342onmain) already covers the cases reported here:M.assignedFn = function() … end→ indexed as a methodM.callbacks = { onStart = function() … end }(including nested tables such astbWnd.tbOnClick = { btnUse = … })local f = function() … end→ indexed askind:function(not a bare variable)CHANGELOG Unreleased already cites both #1616 and #1650.
Linux verify (current
main@b8d46d13, Node v22.19.0)Minimal repro from this issue (
util.lua+handlers.lua) plus thelocal fvariant:No
file handlers.luacollapse. Pre-fix this issue reported onlynamedFn+ a file-level attribution for the missing sites.Nested / static-bracket residual probe also passes (
btnUse, nestedonClick,M["bracketed"]); dynamic bracket keys remain intentionally unguessed (same policy as #1778).No follow-up PR needed — closing as completed by #1778.