Apply upstream PR #1623 by danusha2345 (fix commit aa777063), which also addresses #1616, to the current main base. Preserve the upstream WASM and Rust implementations and regression coverage. Index assigned locals, table members, static string keys and nested callback tables as callable symbols, with calls owned by those symbols. Keep dynamic keys unguessed. Add #1650 to the Unreleased changelog and retain the existing re-index guidance without an extraction-version bump. Verified on Linux x64 with Node 22.19.0: - native kernel build, tsc, asset copy, executable CLI - issue repro: 3 nodes / 2 edges -> 4 nodes / 4 edges - EPR.PowerController::SyncHydroPower is indexed; its caller is client.lua - syncHydroPower depth-2 impact reaches client.lua - extraction/resolution/Lua parity: 844 tests passed (kernel expected) - forced-WASM Lua/Luau extraction/resolution: 20 tests passed Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
co-authored by
Colby McHenry
parent
195888d71f
commit
8c047342cd
@@ -8786,6 +8786,58 @@ function M:send(data) return self end
|
||||
const send = methods.find((m) => m.name === 'send');
|
||||
expect(send?.qualifiedName).toBe('M::send');
|
||||
});
|
||||
|
||||
it('should name function expressions from local, member, and table-field bindings', () => {
|
||||
const code = `
|
||||
local function helper() return 1 end
|
||||
local localFn = function() return helper() end
|
||||
local M = {
|
||||
callbacks = {
|
||||
onStart = function() return helper() end,
|
||||
["onStop"] = function() return helper() end,
|
||||
[DYNAMIC] = function() return helper() end,
|
||||
},
|
||||
}
|
||||
M.assignedFn = function() return helper() end
|
||||
M["bracketFn"] = function() return helper() end
|
||||
localFn()
|
||||
`;
|
||||
const result = extractFromSource('handlers.lua', code);
|
||||
const localFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'localFn');
|
||||
const assignedFn = result.nodes.find(
|
||||
(n) => n.kind === 'method' && n.qualifiedName === 'M::assignedFn'
|
||||
);
|
||||
const onStart = result.nodes.find(
|
||||
(n) => n.kind === 'method' && n.qualifiedName === 'M.callbacks::onStart'
|
||||
);
|
||||
const onStop = result.nodes.find(
|
||||
(n) => n.kind === 'method' && n.qualifiedName === 'M.callbacks::onStop'
|
||||
);
|
||||
const bracketFn = result.nodes.find(
|
||||
(n) => n.kind === 'method' && n.qualifiedName === 'M::bracketFn'
|
||||
);
|
||||
|
||||
expect(localFn).toBeDefined();
|
||||
expect(assignedFn).toBeDefined();
|
||||
expect(onStart).toBeDefined();
|
||||
expect(onStop).toBeDefined();
|
||||
expect(bracketFn).toBeDefined();
|
||||
expect(result.nodes.some((n) => n.name === 'DYNAMIC')).toBe(false);
|
||||
expect(result.nodes.some((n) => n.kind === 'variable' && n.name === 'localFn')).toBe(false);
|
||||
|
||||
for (const callable of [localFn, assignedFn, onStart, onStop, bracketFn]) {
|
||||
expect(
|
||||
result.unresolvedReferences.some(
|
||||
(r) => r.fromNodeId === callable!.id && r.referenceKind === 'calls' && r.referenceName === 'helper'
|
||||
)
|
||||
).toBe(true);
|
||||
}
|
||||
expect(
|
||||
result.unresolvedReferences.some(
|
||||
(r) => r.referenceKind === 'calls' && r.referenceName === 'localFn'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Variable extraction', () => {
|
||||
|
||||
@@ -24,7 +24,7 @@ local function localFn(...)
|
||||
return select("#", ...)
|
||||
end
|
||||
|
||||
-- doc for anonAssigned (variable, initializer invisible)
|
||||
-- doc for anonAssigned (function named from its local binding)
|
||||
local anonAssigned = function(v)
|
||||
return hidden(v)
|
||||
end
|
||||
@@ -68,6 +68,15 @@ M.assigned = function(z)
|
||||
return topFn(z)
|
||||
end
|
||||
|
||||
M.callbacks = {
|
||||
on_start = function()
|
||||
return topFn(17)
|
||||
end,
|
||||
["on_stop"] = function()
|
||||
return topFn(18)
|
||||
end,
|
||||
}
|
||||
|
||||
M.handlers = { on_start = topFn, on_stop = localFn, skipped = missing }
|
||||
local tbl = { cb = topFn, [1] = localFn, nested = { deep_cb = topFn } }
|
||||
|
||||
|
||||
@@ -127,6 +127,22 @@ describe.skipIf(!kernelBuilt)('kernel Lua/Luau extraction parity', () => {
|
||||
// lua functions carry NO isExported (undefined — not false).
|
||||
const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'topFn');
|
||||
expect(fn?.isExported).toBeUndefined();
|
||||
expect(result.nodes.some((n) => n.kind === 'function' && n.name === 'anonAssigned')).toBe(true);
|
||||
expect(result.nodes.some((n) => n.kind === 'method' && n.qualifiedName === 'M::assigned')).toBe(true);
|
||||
expect(
|
||||
result.nodes.some((n) => n.kind === 'method' && n.qualifiedName === 'M.callbacks::on_start')
|
||||
).toBe(true);
|
||||
expect(
|
||||
result.nodes.some((n) => n.kind === 'method' && n.qualifiedName === 'M.callbacks::on_stop')
|
||||
).toBe(true);
|
||||
for (const qualifiedName of ['M::assigned', 'M.callbacks::on_start', 'M.callbacks::on_stop']) {
|
||||
const callable = result.nodes.find((n) => n.qualifiedName === qualifiedName)!;
|
||||
expect(
|
||||
refs.some(
|
||||
(r) => r.fromNodeId === callable.id && r.referenceKind === 'calls' && r.referenceName === 'topFn'
|
||||
)
|
||||
).toBe(true);
|
||||
}
|
||||
// variables DO carry isExported === false.
|
||||
const v = result.nodes.find((n) => n.kind === 'variable' && n.name === 'core');
|
||||
expect(v?.isExported).toBe(false);
|
||||
|
||||
@@ -2055,6 +2055,32 @@ func main() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Lua function-expression resolution (#1616)', () => {
|
||||
it('attributes helper calls to each assigned callable instead of the file node', async () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'util.lua'),
|
||||
`util = {}\nfunction util.helper() return 1 end\nreturn util\n`
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, 'handlers.lua'),
|
||||
`local M = {}\nfunction M.namedFn() return util.helper() end\nM.assignedFn = function() return util.helper() end\nM.callbacks = { onStart = function() return util.helper() end }\nreturn M\n`
|
||||
);
|
||||
|
||||
cg = await CodeGraph.init(tempDir, { index: true });
|
||||
cg.resolveReferences();
|
||||
|
||||
const helper = cg
|
||||
.getNodesByKind('method')
|
||||
.find((n) => n.qualifiedName === 'util::helper');
|
||||
expect(helper).toBeDefined();
|
||||
const callers = cg.getCallers(helper!.id).map((c) => c.node);
|
||||
expect(callers.some((n) => n.qualifiedName === 'M::namedFn')).toBe(true);
|
||||
expect(callers.some((n) => n.qualifiedName === 'M::assignedFn')).toBe(true);
|
||||
expect(callers.some((n) => n.qualifiedName === 'M.callbacks::onStart')).toBe(true);
|
||||
expect(callers.some((n) => n.kind === 'file' && n.filePath === 'handlers.lua')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Watchdog-safe resolution on collision-heavy repos (#1122)', () => {
|
||||
// On a large Java-style repo, per-ref resolution cost is unbounded in the
|
||||
// worst case (a colliding method name whose candidate set misses the LRU
|
||||
|
||||
Reference in New Issue
Block a user