mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-09 22:33:39 +00:00
Three flat O(1) indexes + pure build functions over per-file artifacts. Contract-only; no runtime behavior change yet — consumers (#917 Registry lookups, #915 SCC finalize, #919 ScopeExtractor) wire in later. Each index follows the same shape: - build function: flat input list → frozen immutable index - public interface: readonly Map + get/has/size accessors - first-write-wins on id/filePath collisions (upstream bug signal) - pure, side-effect-free, safe to call repeatedly DefIndex — the global "what is this id?" lookup gitnexus-shared/src/scope-resolution/def-index.ts buildDefIndex(defs: readonly SymbolDefinition[]): DefIndex byId: ReadonlyMap<DefId, SymbolDefinition> Consumed by Registry.lookup (#917) to materialize DefId[] hits back to full SymbolDefinition records. ModuleScopeIndex — `filePath → moduleScopeId` for cross-file hops gitnexus-shared/src/scope-resolution/module-scope-index.ts buildModuleScopeIndex(entries): ModuleScopeIndex byFilePath: ReadonlyMap<string, ScopeId> Consumed by the SCC finalize link pass (#915) to resolve ImportEdge.targetFile to a concrete module scope in constant time. QualifiedNameIndex — cross-kind qualified-name fast path gitnexus-shared/src/scope-resolution/qualified-name-index.ts buildQualifiedNameIndex(defs: readonly SymbolDefinition[]): QualifiedNameIndex byQualifiedName: ReadonlyMap<string, readonly DefId[]> Returns DefId[] (not a single DefId) because partial classes, method overloads, and cross-kind collisions can legitimately share a qualifiedName. Callers filter by acceptedKinds at the lookup site. Consumed by Registry.lookup qualified fast path + resolveTypeRef dotted fallback (#916, #917). Barrel re-exports added to gitnexus-shared/src/index.ts so consumers import from 'gitnexus-shared' rather than deep paths. Tests (gitnexus/test/unit/scope-resolution/, 23 total): def-index.test.ts (6): empty, single def, multiple distinct, first-write-wins collision, missing id returns undefined, byId direct iteration module-scope-index.test.ts (6): empty, single entry, multiple files, first-write-wins on duplicate filePath, missing returns undefined, byFilePath direct iteration qualified-name-index.test.ts (11): empty, single qnamed def, partial classes accumulate, input-order preservation, qname separation, skip undefined/empty qname, pair dedup, cross-kind indexing, frozen-empty-array on miss, direct iteration Verification: - gitnexus-shared + gitnexus build clean (tsc + scripts/build.js) - test/unit/scope-resolution: 23/23 pass - model + shadow + scope-resolution combined: 129/129 pass - No runtime consumer wiring yet — indexes are standalone library functions that #915, #917, #919 will import when ready Depends on #910 (SymbolDefinition, DefId, ScopeId types — already on main). Unblocks #915 (finalize algorithm), #917 (Registry.lookup), #919 (ScopeExtractor materialization).
62 lines
2.2 KiB
TypeScript
62 lines
2.2 KiB
TypeScript
/**
|
|
* Unit tests for `buildModuleScopeIndex` / `ModuleScopeIndex`
|
|
* (RFC #909 Ring 2 SHARED #913).
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { buildModuleScopeIndex, type ModuleScopeEntry, type ScopeId } from 'gitnexus-shared';
|
|
|
|
const entry = (filePath: string, moduleScopeId: ScopeId): ModuleScopeEntry => ({
|
|
filePath,
|
|
moduleScopeId,
|
|
});
|
|
|
|
describe('buildModuleScopeIndex', () => {
|
|
it('builds an empty index from no entries', () => {
|
|
const idx = buildModuleScopeIndex([]);
|
|
expect(idx.size).toBe(0);
|
|
expect(idx.get('src/app.ts')).toBeUndefined();
|
|
expect(idx.has('src/app.ts')).toBe(false);
|
|
});
|
|
|
|
it('round-trips a single entry', () => {
|
|
const idx = buildModuleScopeIndex([entry('src/app.ts', 'scope:src/app.ts#1:0-100:0:Module')]);
|
|
expect(idx.size).toBe(1);
|
|
expect(idx.has('src/app.ts')).toBe(true);
|
|
expect(idx.get('src/app.ts')).toBe('scope:src/app.ts#1:0-100:0:Module');
|
|
});
|
|
|
|
it('stores distinct files under their own scopes', () => {
|
|
const entries: ModuleScopeEntry[] = [
|
|
entry('src/a.ts', 'scope:a'),
|
|
entry('src/b.ts', 'scope:b'),
|
|
entry('src/c.ts', 'scope:c'),
|
|
];
|
|
const idx = buildModuleScopeIndex(entries);
|
|
expect(idx.size).toBe(3);
|
|
expect(idx.get('src/a.ts')).toBe('scope:a');
|
|
expect(idx.get('src/b.ts')).toBe('scope:b');
|
|
expect(idx.get('src/c.ts')).toBe('scope:c');
|
|
});
|
|
|
|
it('first-write-wins when the same filePath appears twice', () => {
|
|
const idx = buildModuleScopeIndex([
|
|
entry('src/app.ts', 'scope:first'),
|
|
entry('src/app.ts', 'scope:second'),
|
|
]);
|
|
expect(idx.size).toBe(1);
|
|
expect(idx.get('src/app.ts')).toBe('scope:first');
|
|
});
|
|
|
|
it('returns undefined for a missing filePath (no throw)', () => {
|
|
const idx = buildModuleScopeIndex([entry('src/a.ts', 'scope:a')]);
|
|
expect(idx.get('src/missing.ts')).toBeUndefined();
|
|
expect(idx.has('src/missing.ts')).toBe(false);
|
|
});
|
|
|
|
it('exposes byFilePath as the underlying read-only Map', () => {
|
|
const idx = buildModuleScopeIndex([entry('src/a.ts', 'scope:a'), entry('src/b.ts', 'scope:b')]);
|
|
const paths = Array.from(idx.byFilePath.keys()).sort();
|
|
expect(paths).toEqual(['src/a.ts', 'src/b.ts']);
|
|
});
|
|
});
|