From 151bb7865fffe3d27e2b971398d6b34f958d05e6 Mon Sep 17 00:00:00 2001 From: taoxin <1015954373@qq.com> Date: Wed, 23 Sep 2026 16:54:12 +0800 Subject: [PATCH] fix(lua): preserve callable binding positions --- gitnexus/src/cli/optional-grammars.ts | 12 +++-- .../core/ingestion/languages/lua/captures.ts | 52 +++++++++++++++++++ .../src/core/ingestion/languages/lua/query.ts | 18 +------ .../integration/resolvers/lua-scope.test.ts | 11 +++- 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/gitnexus/src/cli/optional-grammars.ts b/gitnexus/src/cli/optional-grammars.ts index a94b8f641..4bc2c5499 100644 --- a/gitnexus/src/cli/optional-grammars.ts +++ b/gitnexus/src/cli/optional-grammars.ts @@ -1,13 +1,15 @@ /** * Optional grammar availability check. * - * tree-sitter-dart, -proto, -swift, -kotlin, -zig, and -lua are vendored under + * tree-sitter-dart, -proto, -swift, -kotlin, and -zig are vendored under * vendor/ and loaded from there by absolute path (NEVER copied into * node_modules — see core/tree-sitter/vendored-grammars.ts / #2111). Each - * ships committed platform prebuilds activated via node-gyp-build. All can - * be skipped via GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or - * can silently soft-fail when no prebuild matches the host platform (and a - * source build was unavailable / not attempted). + * ships committed platform prebuilds activated via node-gyp-build. + * tree-sitter-lua is also vendored, but is source-built during install because + * it does not currently ship committed prebuilds. All can be skipped via + * GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1 (postinstall scripts), or can silently + * soft-fail when no prebuild matches the host platform (and a source build was + * unavailable / not attempted). * * Either path produces the same observable: the .node binding is absent * at runtime. This helper detects that condition and surfaces a single diff --git a/gitnexus/src/core/ingestion/languages/lua/captures.ts b/gitnexus/src/core/ingestion/languages/lua/captures.ts index 145a836bb..92b0192c4 100644 --- a/gitnexus/src/core/ingestion/languages/lua/captures.ts +++ b/gitnexus/src/core/ingestion/languages/lua/captures.ts @@ -166,6 +166,56 @@ function collectLuaImportCaptures(root: Parser.SyntaxNode): readonly CaptureMatc return out; } +/** + * Capture callable local bindings by source/destination position. + * + * A tree-sitter query that independently matches `variable_list` and a + * function-valued `expression_list` cross-pairs Lua multi-assignment: + * `local value, callback = 1, function() end` must name only `callback` as a + * closure. Keep the positional pairing in the same structural walk used by + * import capture collection. + */ +function collectLuaCallableBindingCaptures(root: Parser.SyntaxNode): readonly CaptureMatch[] { + const out: CaptureMatch[] = []; + const visit = (node: Parser.SyntaxNode): void => { + if (node.type === 'local_variable_declaration') { + const variables = + node.namedChildren.find((child) => child.type === 'variable_list')?.namedChildren ?? []; + const sources = + node.namedChildren.find((child) => child.type === 'expression_list')?.namedChildren ?? []; + const count = Math.min(variables.length, sources.length); + for (let index = 0; index < count; index++) { + const destination = variables[index]; + const source = sources[index]; + const name = destination?.childForFieldName('name'); + if ( + destination?.type !== 'variable' || + destination.childForFieldName('table') !== null || + destination.childForFieldName('field') !== null || + name?.type !== 'identifier' || + source === undefined + ) { + continue; + } + const declarationKind = + source.type === 'function_definition' + ? '@declaration.function' + : source.type === 'variable' + ? '@declaration.variable' + : undefined; + if (declarationKind === undefined) continue; + out.push({ + [declarationKind]: nodeToCapture(declarationKind, node), + '@declaration.name': nodeToCapture('@declaration.name', name), + }); + } + } + for (const child of node.namedChildren) visit(child); + }; + visit(root); + return out; +} + function collectLuaReturnedNames(root: Parser.SyntaxNode): readonly string[] { const returnedNames: string[] = []; for (const node of root.namedChildren) { @@ -367,6 +417,8 @@ export function emitLuaScopeCaptures( out.push(grouped); } + out.push(...collectLuaCallableBindingCaptures(tree.rootNode)); + // Imports are collected structurally instead of through the generic query: // the AST lists preserve positional local/RHS pairing and support all Lua // string-call forms without producing a cross-product of captures. diff --git a/gitnexus/src/core/ingestion/languages/lua/query.ts b/gitnexus/src/core/ingestion/languages/lua/query.ts index 18748de3a..36ca88011 100644 --- a/gitnexus/src/core/ingestion/languages/lua/query.ts +++ b/gitnexus/src/core/ingestion/languages/lua/query.ts @@ -3,8 +3,8 @@ * * Captures the structural skeleton the central ScopeExtractor consumes: * - scopes: (chunk) root + function bodies - * - declarations: functions, local callable/value bindings, and methods - * (function Obj:m()) + * - declarations: functions and methods (function Obj:m()) + * (local callable/value bindings are captured structurally in captures.ts) * - imports: Lua module-loading calls — the call + its string arg * - references: free calls foo() + member calls obj:m() / obj.f() * @@ -38,20 +38,6 @@ const LUA_SCOPE_QUERY = ` (local_function_definition_statement name: (identifier) @declaration.name) @declaration.function -;; Local callable bindings need a SymbolDefinition before the shared -;; callable-flow pass can attach alias/formal/argument facts to their cells. -;; Keep ordinary variable aliases as Variable nodes, while closure bindings -;; below are Functions so the anonymous-callable convention can resolve them. -(local_variable_declaration - (variable_list - (variable name: (identifier) @declaration.name))) @declaration.variable - -(local_variable_declaration - (variable_list - (variable name: (identifier) @declaration.name)) - (expression_list - value: (function_definition))) @declaration.function - ;; ── Declarations — methods: function Obj:method() / function Obj.field() ──── ;; name is a (variable) with table + method (colon) or table + field (dot). (function_definition_statement diff --git a/gitnexus/test/integration/resolvers/lua-scope.test.ts b/gitnexus/test/integration/resolvers/lua-scope.test.ts index b636c27f7..d36a45d34 100644 --- a/gitnexus/test/integration/resolvers/lua-scope.test.ts +++ b/gitnexus/test/integration/resolvers/lua-scope.test.ts @@ -202,7 +202,7 @@ describe('Lua local callable declarations', () => { `function target() end function entry() local alias = target - local callback = function() end + local value, callback = 1, function() end callback() end `, @@ -218,10 +218,17 @@ end expect( captures.some( (match) => - match['@declaration.function']?.text === 'local callback = function() end' && + match['@declaration.function']?.text === 'local value, callback = 1, function() end' && match['@declaration.name']?.text === 'callback', ), ).toBe(true); + expect( + captures.some( + (match) => + match['@declaration.function']?.text === 'local value, callback = 1, function() end' && + match['@declaration.name']?.text === 'value', + ), + ).toBe(false); expect( captures.some( (match) =>