diff --git a/gitnexus/README.md b/gitnexus/README.md index e9b16ac73..bc3eb07af 100644 --- a/gitnexus/README.md +++ b/gitnexus/README.md @@ -342,7 +342,7 @@ GitNexus supports indexing multiple repositories. Each `gitnexus analyze` regist ## Supported Languages -TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift, Ruby, Dart +TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift, Ruby, Dart, Lua ### Language Feature Matrix @@ -362,6 +362,11 @@ TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Kotlin, Swift, | C | — | — | ✓ | — | ✓ | ✓ | — | ✓ | ✓ | | C++ | — | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | | Dart | ✓ | — | ✓ | ✓ | ✓ | ✓ | — | ✓ | ✓ | +| Lua | ✓ | ✓ | ✓ | ✓* | — | — | — | — | — | + +Lua heritage currently covers middleclass-style `EXTENDS` and `HAS_METHOD` +edges. It does not yet claim complete MRO, `__base` super-call, indirect +receiver, or arity-precision support. **Imports** — cross-file import resolution · **Named Bindings** — `import { X as Y }` / re-export tracking · **Exports** — public/exported symbol detection · **Heritage** — class inheritance, interfaces, mixins · **Type Annotations** — explicit type extraction for receiver resolution · **Constructor Inference** — infer receiver type from constructor calls (`self`/`this` resolution included for all languages) · **Config** — language toolchain config parsing (tsconfig, go.mod, etc.) · **Frameworks** — AST-based framework pattern detection · **Entry Points** — entry point scoring heuristics @@ -520,7 +525,7 @@ results until you re-run `gitnexus analyze --repair-fts` from a shell where the ### Installation fails with native module errors -Some optional language grammars (Dart, Proto, Swift, Kotlin) require native compilation. If they fail, GitNexus still works — those languages will be skipped. To skip them intentionally (no C++ toolchain needed), set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` before installing. +Some optional language grammars (Dart, Proto, Swift, Kotlin, Lua) require native compilation. If they fail, GitNexus still works — those languages will be skipped. To skip them intentionally (no C++ toolchain needed), set `GITNEXUS_SKIP_OPTIONAL_GRAMMARS=1` before installing. If `npm install -g gitnexus` fails on native modules: diff --git a/gitnexus/src/core/ingestion/languages/lua/heritage.ts b/gitnexus/src/core/ingestion/languages/lua/heritage.ts index eae77be5f..b527c6b83 100644 --- a/gitnexus/src/core/ingestion/languages/lua/heritage.ts +++ b/gitnexus/src/core/ingestion/languages/lua/heritage.ts @@ -9,7 +9,7 @@ * was live) and emits: * - EXTENDS from the child Class node to the parent Class node, and * - HAS_METHOD from a class's Class node to its file-top-level Method nodes. - * Both resolve via `nodeLookup` / `graphIdByName`. NO file re-read or re-parse + * Both resolve via `nodeLookup` and finalized scope bindings. NO file re-read or re-parse * (#1983 no-main-thread-re-parse contract). * * Mirrors `emitRubyMixinEdges` (the only other `emitHeritageEdges` impl), but @@ -18,7 +18,10 @@ * decomposition, and the parent is a bare identifier in the source. */ import { type ParsedFile, type NodeLabel } from 'gitnexus-shared'; -import { isClassLike } from '../../scope-resolution/scope/walkers.js'; +import { + isClassLike, + resolveInheritanceBaseInScope, +} from '../../scope-resolution/scope/walkers.js'; import { resolveDefGraphId } from '../../scope-resolution/graph-bridge/ids.js'; import { positionKey, @@ -27,17 +30,14 @@ import { import type { KnowledgeGraph } from '../../../graph/types.js'; import { generateId } from '../../../../lib/utils.js'; import type { LuaCaptureSideChannel } from './capture-side-channel.js'; +import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js'; export function emitLuaHeritageEdges( graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, + scopes?: ScopeResolutionIndexes, ): void { - // name → graphId (global, for parent resolution). First-wins; same-named - // classes in different files are rare in middleclass codebases, and a - // collision here only risks a wrong parent — acceptable for a 0.85-confidence - // heuristic edge (better an imperfect EXTENDS than none). - const graphIdByName = new Map(); // (filePath, name) → graphId (per-file, for child resolution). const graphIdByFileAndName = new Map(); for (const parsed of parsedFiles) { @@ -48,7 +48,6 @@ export function emitLuaHeritageEdges( const qn = def.qualifiedName ?? ''; if (qn.length > 0) { graphIdByFileAndName.set(`${parsed.filePath}::${qn}`, gid); - if (!graphIdByName.has(qn)) graphIdByName.set(qn, gid); } } } @@ -61,10 +60,12 @@ export function emitLuaHeritageEdges( // ── EXTENDS: class("Name", Parent) ────────────────────────────────────── for (const { child, parent } of channel.extendsPairs) { - const childGid = - graphIdByFileAndName.get(`${parsed.filePath}::${child}`) ?? graphIdByName.get(child); - const parentGid = graphIdByName.get(parent); - if (childGid === undefined || parentGid === undefined) continue; + const childGid = graphIdByFileAndName.get(`${parsed.filePath}::${child}`); + if (childGid === undefined || scopes === undefined) continue; + const parentDef = resolveInheritanceBaseInScope(parsed.moduleScope, parent, scopes); + if (parentDef === undefined) continue; + const parentGid = resolveDefGraphId(parentDef.filePath, parentDef, nodeLookup); + if (parentGid === undefined) continue; const edgeKey = `${childGid}->${parentGid}`; if (emittedExtends.has(edgeKey)) continue; emittedExtends.add(edgeKey); @@ -80,8 +81,7 @@ export function emitLuaHeritageEdges( // ── HAS_METHOD: function ClassName:method() / function ClassName.method() ─ for (const { owner, method, defRow } of channel.methodOwners) { - const classGid = - graphIdByFileAndName.get(`${parsed.filePath}::${owner}`) ?? graphIdByName.get(owner); + const classGid = graphIdByFileAndName.get(`${parsed.filePath}::${owner}`); if (classGid === undefined) continue; // Resolve the Method graph node by position (0-based row + simple name). const methodGid = nodeLookup.get( diff --git a/gitnexus/src/core/ingestion/languages/lua/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/lua/scope-resolver.ts index aeb98190c..ba7f9943d 100644 --- a/gitnexus/src/core/ingestion/languages/lua/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/lua/scope-resolver.ts @@ -79,7 +79,7 @@ const luaScopeResolver: ScopeResolver = { // middleclass `class("Name", Parent)` — emits EXTENDS. middleclass has no // syntactic class body, so lexical heritage cannot produce these; the hook - // re-parses for the class() call's parent arg. + // consumes the capture side-channel's class() facts. emitHeritageEdges: emitLuaHeritageEdges, // Lua has globals (`function foo()` is global) — let unresolved free calls diff --git a/gitnexus/src/core/tree-sitter/vendored-grammars.ts b/gitnexus/src/core/tree-sitter/vendored-grammars.ts index a008ab82c..aa4f448ec 100644 --- a/gitnexus/src/core/tree-sitter/vendored-grammars.ts +++ b/gitnexus/src/core/tree-sitter/vendored-grammars.ts @@ -43,7 +43,7 @@ export const vendoredGrammarDir = (packageName: string): string => /** * Load a vendored tree-sitter grammar by its absolute path under `vendor/`. * - * GitNexus vendors five grammars (c/dart/proto/swift/kotlin) inside its own + * 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 * `node_modules`: an undeclared package under `node_modules` is "extraneous" to diff --git a/gitnexus/test/integration/resolvers/lua-scope.test.ts b/gitnexus/test/integration/resolvers/lua-scope.test.ts index e472f5fc7..598e4e9f2 100644 --- a/gitnexus/test/integration/resolvers/lua-scope.test.ts +++ b/gitnexus/test/integration/resolvers/lua-scope.test.ts @@ -136,6 +136,71 @@ return Dog }); }); +// --------------------------------------------------------------------------- +// middleclass heritage: duplicate class names must follow imports or decline +// --------------------------------------------------------------------------- + +describe('Lua scope: middleclass heritage name collisions', () => { + it('resolves an imported duplicate parent to the imported file', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lua-scope-heritage-collision-')); + try { + writeFixtureRepo(tmpDir, { + 'lib/a.lua': `local class = require("lib.class") +local Animal = class("Animal") +return Animal +`, + 'lib/b.lua': `local class = require("lib.class") +local Animal = class("Animal") +return Animal +`, + 'dog.lua': `local class = require("lib.class") +local Animal = require("lib.a") +local Dog = class("Dog", Animal) +return Dog +`, + }); + + const result = await runPipelineFromRepo(tmpDir, () => {}); + const dogExtendsAnimal = getRelationships(result, 'EXTENDS').find( + (edge) => edge.source === 'Dog' && edge.target === 'Animal', + ); + expect(dogExtendsAnimal).toBeDefined(); + expect(dogExtendsAnimal?.targetFilePath).toContain(path.join('lib', 'a.lua')); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }, 60000); + + it('does not guess when duplicate parents are not disambiguated by imports', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lua-scope-heritage-ambiguous-')); + try { + writeFixtureRepo(tmpDir, { + 'lib/a.lua': `local class = require("lib.class") +local Animal = class("Animal") +return Animal +`, + 'lib/b.lua': `local class = require("lib.class") +local Animal = class("Animal") +return Animal +`, + 'dog.lua': `local class = require("lib.class") +local Dog = class("Dog", Animal) +return Dog +`, + }); + + const result = await runPipelineFromRepo(tmpDir, () => {}); + expect( + getRelationships(result, 'EXTENDS').some( + (edge) => edge.source === 'Dog' && edge.target === 'Animal', + ), + ).toBe(false); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }, 60000); +}); + // --------------------------------------------------------------------------- // heritage lifecycle: re-capture with no middleclass must not retain stale // EXTENDS / HAS_METHOD facts from a prior pass (reanalysis regression) diff --git a/gitnexus/test/unit/parser-loader-abi.test.ts b/gitnexus/test/unit/parser-loader-abi.test.ts index 64bd97e99..2310582f2 100644 --- a/gitnexus/test/unit/parser-loader-abi.test.ts +++ b/gitnexus/test/unit/parser-loader-abi.test.ts @@ -14,7 +14,7 @@ import { SupportedLanguages } from '../../src/config/supported-languages.js'; * * - Required grammars MUST load and parse — an ABI-incompatible native * binding (the #1242-class failure) fails here loudly. - * - Optional / vendored grammars (swift/dart/kotlin) must either load OR + * - Optional / vendored grammars (swift/dart/kotlin/lua) must either load OR * cleanly report unavailable — never hard-crash the process. * * Swift is prebuilt-only (no introspectable parser.c) so the static Python