GitNexus/gitnexus/test/unit/scope-resolution/def-index.test.ts
Gergő Magyar ac2012e5ed
feat(shared): DefIndex / ModuleScopeIndex / QualifiedNameIndex (#913, RFC #909 Ring 2 SHARED) (#958)
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).
2026-04-18 15:59:34 +01:00

69 lines
2.4 KiB
TypeScript

/**
* Unit tests for `buildDefIndex` / `DefIndex` (RFC #909 Ring 2 SHARED #913).
*
* Covers: build-from-list, O(1) lookup contract, first-write-wins on
* duplicate `nodeId`, readonly surface.
*/
import { describe, it, expect } from 'vitest';
import { buildDefIndex, type SymbolDefinition } from 'gitnexus-shared';
const makeDef = (overrides: Partial<SymbolDefinition> = {}): SymbolDefinition => ({
nodeId: 'def:test',
filePath: 'src/test.ts',
type: 'Method',
...overrides,
});
describe('buildDefIndex', () => {
it('builds an empty index from an empty input', () => {
const idx = buildDefIndex([]);
expect(idx.size).toBe(0);
expect(idx.get('anything')).toBeUndefined();
expect(idx.has('anything')).toBe(false);
});
it('stores a single def and round-trips by nodeId', () => {
const def = makeDef({ nodeId: 'def:User.save' });
const idx = buildDefIndex([def]);
expect(idx.size).toBe(1);
expect(idx.has('def:User.save')).toBe(true);
expect(idx.get('def:User.save')).toBe(def); // reference identity
});
it('stores multiple defs under their distinct ids', () => {
const a = makeDef({ nodeId: 'def:A' });
const b = makeDef({ nodeId: 'def:B' });
const c = makeDef({ nodeId: 'def:C' });
const idx = buildDefIndex([a, b, c]);
expect(idx.size).toBe(3);
expect(idx.get('def:A')).toBe(a);
expect(idx.get('def:B')).toBe(b);
expect(idx.get('def:C')).toBe(c);
});
it('first-write-wins on duplicate nodeId', () => {
const first = makeDef({ nodeId: 'def:dup', returnType: 'Original' });
const second = makeDef({ nodeId: 'def:dup', returnType: 'Shadow' });
const idx = buildDefIndex([first, second]);
expect(idx.size).toBe(1);
expect(idx.get('def:dup')).toBe(first);
expect(idx.get('def:dup')?.returnType).toBe('Original');
});
it("returns undefined for a missing id (doesn't throw)", () => {
const idx = buildDefIndex([makeDef({ nodeId: 'def:A' })]);
expect(idx.get('def:missing')).toBeUndefined();
expect(idx.has('def:missing')).toBe(false);
});
it('exposes byId as the underlying read-only Map for direct iteration', () => {
const a = makeDef({ nodeId: 'def:A' });
const b = makeDef({ nodeId: 'def:B' });
const idx = buildDefIndex([a, b]);
const entries = Array.from(idx.byId.entries())
.map(([id]) => id)
.sort();
expect(entries).toEqual(['def:A', 'def:B']);
});
});