mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
fix(lua): resolve heritage parents through imports
This commit is contained in:
parent
52b74d02b3
commit
cc060f7ab1
6 changed files with 89 additions and 19 deletions
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, string>();
|
||||
// (filePath, name) → graphId (per-file, for child resolution).
|
||||
const graphIdByFileAndName = new Map<string, string>();
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue