From a5611a3f86bb0b9e9e5a629ff616844ea4e793ab Mon Sep 17 00:00:00 2001 From: taoxin <1015954373@qq.com> Date: Mon, 24 Aug 2026 16:44:03 +0800 Subject: [PATCH] fix(lua): address latest PR review findings --- .../language-classification.ts | 4 +- .../core/ingestion/import-resolvers/utils.ts | 2 +- .../core/ingestion/languages/lua/interpret.ts | 11 +++++- .../src/core/ingestion/utils/call-analysis.ts | 37 +++++++++++++++++++ .../integration/resolvers/lua-scope.test.ts | 34 +++++++++++++++++ gitnexus/test/unit/call-form.test.ts | 21 +++++++++++ gitnexus/test/unit/language-skip.test.ts | 2 +- gitnexus/test/unit/parser-loader-abi.test.ts | 9 ++--- 8 files changed, 108 insertions(+), 12 deletions(-) diff --git a/gitnexus-shared/src/scope-resolution/language-classification.ts b/gitnexus-shared/src/scope-resolution/language-classification.ts index 4941a0081..ab2e5633d 100644 --- a/gitnexus-shared/src/scope-resolution/language-classification.ts +++ b/gitnexus-shared/src/scope-resolution/language-classification.ts @@ -11,8 +11,8 @@ * ruby, rust, php, kotlin, swift, dart * - experimental: vue (embedded-language / SFC complexity), * cobol (regex-provider path), - * lua (definition-only legacy DAG path; scope-resolution - * hooks pending — Phase B) + * lua (scope-resolution provider is active; broader + * language-contract coverage is still experimental) * - quarantined: (none) */ diff --git a/gitnexus/src/core/ingestion/import-resolvers/utils.ts b/gitnexus/src/core/ingestion/import-resolvers/utils.ts index 4a10d79c9..344052cb0 100644 --- a/gitnexus/src/core/ingestion/import-resolvers/utils.ts +++ b/gitnexus/src/core/ingestion/import-resolvers/utils.ts @@ -55,7 +55,7 @@ export const EXTENSIONS = [ ]; /** Lua module extensions used only by Lua import resolvers. */ -export const LUA_EXTENSIONS = ['', '.lua', '/init.lua'] as const; +export const LUA_EXTENSIONS = ['.lua', '/init.lua'] as const; /** * Try to match a path (with extensions) against the known file set. diff --git a/gitnexus/src/core/ingestion/languages/lua/interpret.ts b/gitnexus/src/core/ingestion/languages/lua/interpret.ts index 9e153d166..7da295f3e 100644 --- a/gitnexus/src/core/ingestion/languages/lua/interpret.ts +++ b/gitnexus/src/core/ingestion/languages/lua/interpret.ts @@ -19,14 +19,21 @@ */ import type { CaptureMatch, ParsedImport } from 'gitnexus-shared'; +function stripLuaString(s: string): string { + const long = s.match(/^\[(=*)\[([\s\S]*)\]\1\]$/); + return long ? long[2] : stripQuotes(s); +} + function stripQuotes(s: string): string { - return s.replace(/^["']|["']$/g, ''); + const quoted = s.match(/^(?:(["'])([\s\S]*)\1|\[(=*)\]([\s\S]*)\]\3\])$/); + if (!quoted) return s; + return quoted[2] ?? quoted[4] ?? ''; } export function interpretLuaImport(captures: CaptureMatch): ParsedImport | null { const source = captures['@import.source']?.text; if (source === undefined) return null; - const targetRaw = stripQuotes(source); + const targetRaw = stripLuaString(source); if (!targetRaw) return null; const localName = captures['@import.localName']?.text; if (localName) { diff --git a/gitnexus/src/core/ingestion/utils/call-analysis.ts b/gitnexus/src/core/ingestion/utils/call-analysis.ts index 30224d29c..a95cb955a 100644 --- a/gitnexus/src/core/ingestion/utils/call-analysis.ts +++ b/gitnexus/src/core/ingestion/utils/call-analysis.ts @@ -155,6 +155,23 @@ export const inferCallForm = (callNode: SyntaxNode, nameNode: SyntaxNode): CallF return 'member'; } + // Lua tree-sitter represents `obj:method()` / `obj.field()` as a `call` + // whose `function` child is a `variable` carrying `table` plus `method` or + // `field`; unlike the other grammars, the captured name is not wrapped in a + // member-access node. + if (callNode.type === 'call') { + const callee = callNode.childForFieldName('function'); + const table = callNode.childForFieldName('table') ?? callee?.childForFieldName('table'); + const member = + callNode.childForFieldName('method') ?? + callNode.childForFieldName('field') ?? + callee?.childForFieldName('method') ?? + callee?.childForFieldName('field'); + if (callee?.type === 'variable' && table && member) { + return 'member'; + } + } + // 5. Scoped calls (Rust Foo::new(), C++ ns::func()): treat as free // The receiver is a type, not an instance — handled differently in Phase 3 if (nameParent && SCOPED_CALL_NODE_TYPES.has(nameParent.type)) { @@ -226,6 +243,14 @@ export const extractReceiverName = (nameNode: SyntaxNode): string | undefined => receiver = parent.childForFieldName('receiver'); } + // Lua: the receiver is the `table` field of the call's `function` variable. + if (!receiver && callNode.type === 'call') { + const callee = callNode.childForFieldName('function'); + if (callee?.type === 'variable') { + receiver = callNode.childForFieldName('table') ?? callee.childForFieldName('table'); + } + } + // PHP scoped_call_expression (parent::method(), self::method()): // nameNode's direct parent IS the scoped_call_expression (name is a direct child) if ( @@ -274,6 +299,12 @@ export const extractReceiverName = (nameNode: SyntaxNode): string | undefined => if (!receiver) return undefined; + // Lua colon calls wrap a simple receiver in a `variable` node. + if (receiver.type === 'variable') { + const name = receiver.childForFieldName('name'); + if (name?.type === 'identifier') return name.text; + } + // Only capture simple identifiers — refuse complex expressions if (SIMPLE_RECEIVER_TYPES.has(receiver.type)) { return receiver.text; @@ -328,6 +359,12 @@ export const extractReceiverNode = (nameNode: SyntaxNode): SyntaxNode | undefine receiver = parent.childForFieldName('receiver'); } + // Lua: the receiver is the `table` field of the call's `function` variable. + if (!receiver && callNode.type === 'call') { + const callee = callNode.childForFieldName('function'); + if (callee?.type === 'variable') receiver = callNode.childForFieldName('table'); + } + if ( !receiver && (parent.type === 'scoped_call_expression' || callNode.type === 'scoped_call_expression') diff --git a/gitnexus/test/integration/resolvers/lua-scope.test.ts b/gitnexus/test/integration/resolvers/lua-scope.test.ts index 27815e570..f58c10fe0 100644 --- a/gitnexus/test/integration/resolvers/lua-scope.test.ts +++ b/gitnexus/test/integration/resolvers/lua-scope.test.ts @@ -23,6 +23,7 @@ import { SupportedLanguages, type BindingRef, type ScopeId } from 'gitnexus-shar import { emitLuaScopeCaptures } from '../../../src/core/ingestion/languages/lua/index.js'; import { collectLuaCaptureSideChannel } from '../../../src/core/ingestion/languages/lua/capture-side-channel.js'; import { luaScopeResolver } from '../../../src/core/ingestion/languages/lua/scope-resolver.js'; +import { interpretLuaImport } from '../../../src/core/ingestion/languages/lua/interpret.js'; function writeFixtureRepo(root: string, files: Record): void { for (const [rel, content] of Object.entries(files)) { @@ -56,6 +57,22 @@ describe('Lua scope resolver import extensions', () => { const files = new Set(['main.lua', 'foo.ts', 'foo.lua']); expect(luaScopeResolver.resolveImportTarget('foo', 'main.lua', files)).toBe('foo.lua'); }); + + it('does not bind an extensionless collision ahead of the Lua module', () => { + const files = new Set(['main.lua', 'foo', 'foo.lua']); + expect(luaScopeResolver.resolveImportTarget('foo', 'main.lua', files)).toBe('foo.lua'); + }); + + it('unwraps quoted and long-bracket require sources', () => { + const sources = ['"lib.util"', "'lib.util'", '[[lib.util]]', '[=[lib.util]=]']; + for (const source of sources) { + expect( + interpretLuaImport({ + '@import.source': { text: source }, + } as never), + ).toEqual({ kind: 'wildcard', targetRaw: 'lib.util' }); + } + }); }); // --------------------------------------------------------------------------- @@ -128,6 +145,23 @@ describe('Lua scope: bare require import', () => { fs.rmSync(tmpDir, { recursive: true, force: true }); } }, 60000); + + it('resolves a long-bracket require source in the graph', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lua-scope-long-require-')); + try { + writeFixtureRepo(tmpDir, { + 'lib/util.lua': 'return {}\n', + 'main.lua': 'require([[lib.util]])\n', + }); + const result = await runPipelineFromRepo(tmpDir, () => {}); + const imports = getRelationships(result, 'IMPORTS').filter( + (e) => e.sourceFilePath?.includes('main.lua') && e.targetFilePath?.includes('util.lua'), + ); + expect(imports).toHaveLength(1); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }, 60000); }); // --------------------------------------------------------------------------- diff --git a/gitnexus/test/unit/call-form.test.ts b/gitnexus/test/unit/call-form.test.ts index e659c29f3..38d97e9ed 100644 --- a/gitnexus/test/unit/call-form.test.ts +++ b/gitnexus/test/unit/call-form.test.ts @@ -20,6 +20,7 @@ import { getProvider } from '../../src/core/ingestion/languages/index.js'; // Vendored grammar — loaded from vendor/ by absolute path, never node_modules (#2111). const Kotlin = requireVendoredGrammar('tree-sitter-kotlin'); +const Lua = requireVendoredGrammar('tree-sitter-lua'); /** * Helper: parse code, run the language query, and return all @call captures @@ -97,6 +98,26 @@ describe('inferCallForm', () => { }); }); + describe('Lua', () => { + it('detects colon member calls', () => { + parser.setLanguage(Lua); + const captures = extractCallCaptures(parser, 'util:answer()', SupportedLanguages.Lua); + const match = captures.find((c) => c.calledName === 'answer'); + expect(match).toBeDefined(); + expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); + expect(extractReceiverName(match!.nameNode)).toBe('util'); + }); + + it('detects dot member calls', () => { + parser.setLanguage(Lua); + const captures = extractCallCaptures(parser, 'util.answer()', SupportedLanguages.Lua); + const match = captures.find((c) => c.calledName === 'answer'); + expect(match).toBeDefined(); + expect(inferCallForm(match!.callNode, match!.nameNode)).toBe('member'); + expect(extractReceiverName(match!.nameNode)).toBe('util'); + }); + }); + describe('Java', () => { it('detects free call (no object)', () => { parser.setLanguage(Java); diff --git a/gitnexus/test/unit/language-skip.test.ts b/gitnexus/test/unit/language-skip.test.ts index 6a60804bd..d2d011e31 100644 --- a/gitnexus/test/unit/language-skip.test.ts +++ b/gitnexus/test/unit/language-skip.test.ts @@ -32,7 +32,7 @@ describe('isLanguageAvailable', () => { if (isGrammarRuntimeSkipped(SupportedLanguages.Lua)) { expect(available).toBe(false); } else { - expect(typeof available).toBe('boolean'); + expect(available).toBe(true); } }); diff --git a/gitnexus/test/unit/parser-loader-abi.test.ts b/gitnexus/test/unit/parser-loader-abi.test.ts index 7c1ac68db..994da9f9d 100644 --- a/gitnexus/test/unit/parser-loader-abi.test.ts +++ b/gitnexus/test/unit/parser-loader-abi.test.ts @@ -203,13 +203,10 @@ describe('parser-loader ABI load-smoke (#1922)', () => { return; } + const crlf = String.fromCharCode(13, 10); const snippets = [ - String.raw`local value = "line\ -continued" -`, - String.raw`local value = 'line\ -continued' -`, + `local value = "line\\${crlf}continued"${crlf}`, + `local value = 'line\\${crlf}continued'${crlf}`, ]; for (const snippet of snippets) { const parser = new Parser();