From 16f3641ca54021d9c4eb28f7ab2032162e13246c Mon Sep 17 00:00:00 2001 From: taoxin <1015954373@qq.com> Date: Fri, 21 Aug 2026 16:42:32 +0800 Subject: [PATCH] fix(lua): address review findings --- .../src/core/ingestion/languages/lua/query.ts | 19 +++++++++++---- .../ingestion/languages/lua/scope-resolver.ts | 6 +++-- .../src/core/tree-sitter/parser-loader.ts | 4 ++-- .../src/core/tree-sitter/vendored-grammars.ts | 6 +++-- .../integration/resolvers/lua-scope.test.ts | 23 +++++++++++++++++-- gitnexus/test/unit/language-skip.test.ts | 15 +++++++++--- gitnexus/test/unit/parser-loader-abi.test.ts | 11 +++++++++ .../unit/parser-loader-skip-optional.test.ts | 4 +++- .../external-import-conformance.test.ts | 12 ++++++++++ ...import-target-index-reuse.contract.test.ts | 13 +++++++++++ gitnexus/vendor/tree-sitter-lua/src/scanner.c | 6 ++--- 11 files changed, 99 insertions(+), 20 deletions(-) diff --git a/gitnexus/src/core/ingestion/languages/lua/query.ts b/gitnexus/src/core/ingestion/languages/lua/query.ts index 0ac552336..da1cf5f4c 100644 --- a/gitnexus/src/core/ingestion/languages/lua/query.ts +++ b/gitnexus/src/core/ingestion/languages/lua/query.ts @@ -66,7 +66,9 @@ const LUA_SCOPE_QUERY = ` ;; receiver-linkable: collectNamespaceTargets registers X -> target file, ;; and the member call resolves via Case 1 (namespace). ;; 2. bare require("a.b.c") (side-effect, no binding) — emits wildcard; -;; the IMPORTS edge still materializes but no receiver is bound. +;; the IMPORTS edge still materializes but no receiver is bound. The +;; statement-level patterns below only accept direct chunk/block children, +;; so a bound require is not matched a second time through its nested call. (local_variable_declaration (variable_list (variable name: (identifier) @import.localName)) (expression_list @@ -75,10 +77,17 @@ const LUA_SCOPE_QUERY = ` arguments: (argument_list (expression_list (string) @import.source))) (#eq? @_req "require"))) @import.statement -(call - function: (variable name: (identifier) @_req) - arguments: (argument_list (expression_list (string) @import.source)) - (#eq? @_req "require")) @import.statement +(chunk + (call + function: (variable name: (identifier) @_req) + arguments: (argument_list (expression_list (string) @import.source)) + (#eq? @_req "require")) @import.statement) + +(block + (call + function: (variable name: (identifier) @_req) + arguments: (argument_list (expression_list (string) @import.source)) + (#eq? @_req "require")) @import.statement) ;; ── References — free calls: foo() ─────────────────────────────────────────── ;; Also matches the require call — harmless: require has no def, so it stays an diff --git a/gitnexus/src/core/ingestion/languages/lua/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/lua/scope-resolver.ts index ba7f9943d..c0abc65b9 100644 --- a/gitnexus/src/core/ingestion/languages/lua/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/lua/scope-resolver.ts @@ -57,8 +57,10 @@ const luaScopeResolver: ScopeResolver = { _cachedIndex = buildSuffixIndex(list, list); _cachedSet = allFilePaths; } - const list = [...allFilePaths]; - return suffixResolve(parts, list, list, _cachedIndex ?? undefined); + // Once the suffix index exists, suffixResolve only consults the index; + // passing a spread copy here would still traverse the entire workspace on + // every require() despite the cache (#2909 contract). + return suffixResolve(parts, [], [], _cachedIndex ?? undefined); }, // Lua: default local-first-then-imports merge (no language-specific precedence). diff --git a/gitnexus/src/core/tree-sitter/parser-loader.ts b/gitnexus/src/core/tree-sitter/parser-loader.ts index 876c12e0c..9274bc7ae 100644 --- a/gitnexus/src/core/tree-sitter/parser-loader.ts +++ b/gitnexus/src/core/tree-sitter/parser-loader.ts @@ -217,7 +217,7 @@ const loadCache = new Map(); const logged = new Set(); /** - * Runtime opt-out for genuinely-optional grammars (Swift/Dart/Kotlin). + * Runtime opt-out for genuinely-optional grammars (Swift/Dart/Kotlin/Lua). * * `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` has historically been an *install-time* * env only — the postinstall build scripts read it to skip building the @@ -231,7 +231,7 @@ const logged = new Set(); * Accepts `1` / `true` / `all` / `*` (every skippable grammar), or a * comma-separated list of language ids and/or package names * (e.g. `swift,tree-sitter-dart`). Only grammars flagged `userSkippable` (the - * genuinely-optional swift/dart/kotlin) can be skipped — required dependencies + * genuinely-optional swift/dart/kotlin/lua) can be skipped — required dependencies * routed through the optional machinery for ABI safety (C) carry no * `userSkippable` and are never skippable here. */ diff --git a/gitnexus/src/core/tree-sitter/vendored-grammars.ts b/gitnexus/src/core/tree-sitter/vendored-grammars.ts index aa4f448ec..daa3e61f6 100644 --- a/gitnexus/src/core/tree-sitter/vendored-grammars.ts +++ b/gitnexus/src/core/tree-sitter/vendored-grammars.ts @@ -44,8 +44,10 @@ export const vendoredGrammarDir = (packageName: string): string => * Load a vendored tree-sitter grammar by its absolute path under `vendor/`. * * GitNexus vendors six grammars (c/dart/proto/swift/kotlin/lua) inside its own - * package under `vendor/`, each shipping committed per-platform prebuilds. They - * are deliberately NOT npm dependencies and must NEVER be copied into + * package under `vendor/`. Grammars load a matching committed prebuild when + * available; Lua is source-built by the grammar workflow/postinstall because + * it does not currently ship committed prebuilds. They are deliberately NOT + * npm dependencies and must NEVER be copied into * `node_modules`: an undeclared package under `node_modules` is "extraneous" to * every subsequent `npm`/`npx` arborist reify, which prunes or relocates it. * That is the root cause of #2111 / #1728 — on Windows the relocation throws diff --git a/gitnexus/test/integration/resolvers/lua-scope.test.ts b/gitnexus/test/integration/resolvers/lua-scope.test.ts index 598e4e9f2..c14021c65 100644 --- a/gitnexus/test/integration/resolvers/lua-scope.test.ts +++ b/gitnexus/test/integration/resolvers/lua-scope.test.ts @@ -63,10 +63,10 @@ run() it('emits IMPORTS edge from main.lua to lib/util.lua', () => { const imports = getRelationships(result, 'IMPORTS'); - const imp = imports.find( + const utilImports = imports.filter( (e) => e.sourceFilePath?.includes('main.lua') && e.targetFilePath?.includes('util.lua'), ); - expect(imp).toBeDefined(); + expect(utilImports).toHaveLength(1); }); it('resolves run -> util.answer() as CALLS edge to util.lua', () => { @@ -83,6 +83,25 @@ run() }); }); +describe('Lua scope: bare require import', () => { + it('emits one IMPORTS edge for an unbound side-effect require', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lua-scope-bare-import-')); + 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); +}); + // --------------------------------------------------------------------------- // middleclass: class("Name", Parent) → EXTENDS + HAS_METHOD across files // --------------------------------------------------------------------------- diff --git a/gitnexus/test/unit/language-skip.test.ts b/gitnexus/test/unit/language-skip.test.ts index 4f7de7887..6a60804bd 100644 --- a/gitnexus/test/unit/language-skip.test.ts +++ b/gitnexus/test/unit/language-skip.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { isLanguageAvailable, loadLanguage } from '../../src/core/tree-sitter/parser-loader.js'; +import { + isGrammarRuntimeSkipped, + isLanguageAvailable, + loadLanguage, +} from '../../src/core/tree-sitter/parser-loader.js'; import { SupportedLanguages } from '../../src/config/supported-languages.js'; describe('isLanguageAvailable', () => { @@ -23,8 +27,13 @@ describe('isLanguageAvailable', () => { expect(isLanguageAvailable(SupportedLanguages.Swift)).toBe(true); }); - it('returns true for Lua in the default install (vendored grammar)', () => { - expect(isLanguageAvailable(SupportedLanguages.Lua)).toBe(true); + it('reports Lua availability according to the optional grammar state', () => { + const available = isLanguageAvailable(SupportedLanguages.Lua); + if (isGrammarRuntimeSkipped(SupportedLanguages.Lua)) { + expect(available).toBe(false); + } else { + expect(typeof available).toBe('boolean'); + } }); it('handles Kotlin based on optional dependency availability', () => { diff --git a/gitnexus/test/unit/parser-loader-abi.test.ts b/gitnexus/test/unit/parser-loader-abi.test.ts index 2310582f2..3b19b66cf 100644 --- a/gitnexus/test/unit/parser-loader-abi.test.ts +++ b/gitnexus/test/unit/parser-loader-abi.test.ts @@ -168,4 +168,15 @@ describe('parser-loader ABI load-smoke (#1922)', () => { expect(tree.rootNode.type).toBe(testCase.rootType); }); } + + it('repeatedly creates and parses Lua parsers without stale scanner state', () => { + const grammar = getLanguageGrammar(SupportedLanguages.Lua); + for (let i = 0; i < 16; i += 1) { + const parser = new Parser(); + parser.setLanguage(grammar as Parameters[0]); + const snippet = i % 2 === 0 ? 'local x = "lua"\n' : '-- comment\nlocal x = [[lua]]\n'; + const tree = parser.parse(snippet); + expect(tree.rootNode.type).toBe('chunk'); + } + }); }); diff --git a/gitnexus/test/unit/parser-loader-skip-optional.test.ts b/gitnexus/test/unit/parser-loader-skip-optional.test.ts index ae8cff396..32413c93c 100644 --- a/gitnexus/test/unit/parser-loader-skip-optional.test.ts +++ b/gitnexus/test/unit/parser-loader-skip-optional.test.ts @@ -6,7 +6,7 @@ import { SupportedLanguages } from '../../src/config/supported-languages.js'; * * `GITNEXUS_SKIP_OPTIONAL_GRAMMARS` used to be an install-time-only env (the * postinstall build scripts read it). `parser-loader` now also honors it at - * analyze time: when set, genuinely-optional grammars (swift/dart/kotlin) + * analyze time: when set, genuinely-optional grammars (swift/dart/kotlin/lua) * report unavailable so the ingestion pipeline skips their files, mirroring a * genuinely-absent binding. Grammars that are required `dependencies` routed * through the optional machinery for ABI safety (C — `severity: 'error'`) are @@ -39,6 +39,7 @@ describe('parser-loader GITNEXUS_SKIP_OPTIONAL_GRAMMARS runtime gate', () => { expect(isLanguageAvailable(SupportedLanguages.Swift)).toBe(false); expect(isLanguageAvailable(SupportedLanguages.Dart)).toBe(false); expect(isLanguageAvailable(SupportedLanguages.Kotlin)).toBe(false); + expect(isLanguageAvailable(SupportedLanguages.Lua)).toBe(false); }); it('skip=all/true/* also skip every optional grammar', async () => { @@ -47,6 +48,7 @@ describe('parser-loader GITNEXUS_SKIP_OPTIONAL_GRAMMARS runtime gate', () => { expect(isLanguageAvailable(SupportedLanguages.Swift), `value=${v}`).toBe(false); expect(isLanguageAvailable(SupportedLanguages.Dart), `value=${v}`).toBe(false); expect(isLanguageAvailable(SupportedLanguages.Kotlin), `value=${v}`).toBe(false); + expect(isLanguageAvailable(SupportedLanguages.Lua), `value=${v}`).toBe(false); } }); diff --git a/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts b/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts index 870a81bf4..0983cb658 100644 --- a/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts +++ b/gitnexus/test/unit/scope-resolution/external-import-conformance.test.ts @@ -312,6 +312,17 @@ const CASES: ReadonlyMap = new Map([ reachesDecoy: 'package:app/http.dart', }, ], + [ + SupportedLanguages.Lua, + { + files: ['vendor/ghost.lua', 'lib/main.lua'], + fromFile: 'lib/main.lua', + resolutionConfig: undefined, + external: 'external.ghost', + decoy: 'vendor/ghost.lua', + reachesDecoy: 'vendor.ghost', + }, + ], [ SupportedLanguages.Swift, { @@ -376,6 +387,7 @@ const KNOWN_GAPS: ReadonlyMap = new Map `lib/generators.rb`'], [SupportedLanguages.PHP, '`Vendor\\Ghost\\Missing` -> `lib/Legacy/Missing.php`'], [SupportedLanguages.Dart, '`package:http/http.dart` -> `lib/http.dart`'], + [SupportedLanguages.Lua, '`external.ghost` -> `vendor/ghost.lua`'], [SupportedLanguages.Swift, '`Foundation` -> `Sources/Foundation/Thing.swift`'], [SupportedLanguages.C, '`stdio.h` -> `src/stdio.h`'], [SupportedLanguages.CPlusPlus, '`cstdio.h` -> `src/cstdio.h`'], diff --git a/gitnexus/test/unit/scope-resolution/import-target-index-reuse.contract.test.ts b/gitnexus/test/unit/scope-resolution/import-target-index-reuse.contract.test.ts index 8f1243566..0fa76ed29 100644 --- a/gitnexus/test/unit/scope-resolution/import-target-index-reuse.contract.test.ts +++ b/gitnexus/test/unit/scope-resolution/import-target-index-reuse.contract.test.ts @@ -528,6 +528,19 @@ const FIXTURES: ReadonlyMap = new Map< minimumParsedFileReads: 0, }, ], + [ + SupportedLanguages.Lua, + { + files: ['lib/models.lua', 'lib/main.lua'], + fromFile: 'lib/main.lua', + resolutionConfig: undefined, + missTarget: (i) => `vendor${i}.ghost`, + hitTarget: 'lib.models', + parsedImport: IGNORES_CONTEXT, + minimumScans: 1, + minimumParsedFileReads: 0, + }, + ], ]); /** diff --git a/gitnexus/vendor/tree-sitter-lua/src/scanner.c b/gitnexus/vendor/tree-sitter-lua/src/scanner.c index 1d0633494..3a06e6218 100644 --- a/gitnexus/vendor/tree-sitter-lua/src/scanner.c +++ b/gitnexus/vendor/tree-sitter-lua/src/scanner.c @@ -50,7 +50,7 @@ struct ScannerState void *tree_sitter_lua_external_scanner_create() { - return malloc(sizeof(struct ScannerState)); + return calloc(1, sizeof(struct ScannerState)); } void tree_sitter_lua_external_scanner_destroy(void *payload) @@ -278,12 +278,12 @@ bool tree_sitter_lua_external_scanner_scan(void *payload, TSLexer *lexer, const if (valid_symbols[STRING_START]) { - // try to match a short single-quoted string's start (") + // try to match a short single-quoted string's start (') if (consume_if(lexer, SQ_STRING_DELIMITER)) { state->started = SHORT_SQ_STRING; } - // try to match a short double-quoted string's start (') + // try to match a short double-quoted string's start (") else if (consume_if(lexer, DQ_STRING_DELIMITER)) { state->started = SHORT_DQ_STRING;