GitNexus/gitnexus/test/unit/scope-resolution/method-dispatch-index.test.ts
Gergő Magyar 5d76dbcfa2
feat(shared): MethodDispatchIndex materialized view over HeritageMap (#914, RFC #909 Ring 2 SHARED) (#960)
Implements RFC §3.1 `MethodDispatchIndex`: a two-way materialized view
keyed by `DefId` for O(1) method-dispatch resolution:

  - `mroByOwnerDefId`       — owner class → full MRO ancestor chain
                              (excludes self, per-language strategy order)
  - `implsByInterfaceDefId` — interface/trait → classes that implement it

**Not an MRO implementation.** `buildMethodDispatchIndex` is a pure
aggregator that calls back into caller-provided `computeMro` and
`implementsOf` functions. The five existing strategies (Python C3, Ruby
kind-aware, Java/Kotlin linear, Rust qualified-syntax, COBOL none) stay
where they are today (`model/resolve.ts`, `languages/ruby.ts`); this index
does not reimplement them.

Why callbacks rather than a shared registry: the strategies depend on the
CLI's `HeritageMap` + `SemanticModel`. Migrating both to `gitnexus-shared`
is out of scope for #914; callbacks let the shared build stay pure.

Module placement: `gitnexus-shared/src/scope-resolution/method-dispatch-index.ts`
for consistency with the other RFC §3.1 indexes (#913 DefIndex /
ModuleScopeIndex / QualifiedNameIndex; #916 resolveTypeRef).

Safety surface mirrors sibling indexes:
  - First-write-wins on duplicate owners.
  - Repeated (interface, owner) pairs deduplicated.
  - Stored arrays are `Object.freeze`d; caller mutation of the source
    array does not leak into the index.
  - Miss returns a shared frozen empty array.

Tests (19, all passing): empty input, single-inheritance chain, Python
C3 diamond, Java BFS, Ruby kind-aware mixin, Rust qualified-syntax empty,
interface inversion (single, multiple, ordered), dedup within and across
callback calls, frozen miss + bucket arrays, callback-array isolation,
readonly Map iteration.

Closes part of #909.
2026-04-18 16:28:46 +01:00

216 lines
8.7 KiB
TypeScript

/**
* Unit tests for `buildMethodDispatchIndex` / `MethodDispatchIndex`
* (RFC #909 Ring 2 SHARED #914).
*
* Covers: empty input, single-inheritance chain, diamond inheritance (caller-
* determined MRO order), interface-only dispatch, multiple implementors,
* dedup, first-write-wins, C3 vs BFS strategy parity (both honored verbatim),
* readonly surface + frozen output.
*/
import { describe, it, expect } from 'vitest';
import { buildMethodDispatchIndex, type MethodDispatchInput, type DefId } from 'gitnexus-shared';
// ─── Test helpers ───────────────────────────────────────────────────────────
const input = (
owners: readonly DefId[],
mroByOwner: Record<DefId, readonly DefId[]>,
implementsByOwner: Record<DefId, readonly DefId[]> = {},
): MethodDispatchInput => ({
owners,
computeMro: (owner) => mroByOwner[owner] ?? [],
implementsOf: (owner) => implementsByOwner[owner] ?? [],
});
// ─── Tests ──────────────────────────────────────────────────────────────────
describe('buildMethodDispatchIndex', () => {
describe('empty / degenerate inputs', () => {
it('builds an empty index from no owners', () => {
const idx = buildMethodDispatchIndex(input([], {}));
expect(idx.mroByOwnerDefId.size).toBe(0);
expect(idx.implsByInterfaceDefId.size).toBe(0);
expect(idx.mroFor('anything')).toEqual([]);
expect(idx.implementorsOf('anything')).toEqual([]);
});
it('indexes an owner with no parents and no interfaces', () => {
const idx = buildMethodDispatchIndex(input(['def:A'], { 'def:A': [] }));
expect(idx.mroByOwnerDefId.size).toBe(1);
expect(idx.implsByInterfaceDefId.size).toBe(0);
expect(idx.mroFor('def:A')).toEqual([]);
});
});
describe('MRO materialization (single / multi inheritance)', () => {
it('records a single-inheritance chain verbatim from the callback', () => {
// A extends B extends C
const idx = buildMethodDispatchIndex(
input(['def:A', 'def:B', 'def:C'], {
'def:A': ['def:B', 'def:C'],
'def:B': ['def:C'],
'def:C': [],
}),
);
expect(idx.mroFor('def:A')).toEqual(['def:B', 'def:C']);
expect(idx.mroFor('def:B')).toEqual(['def:C']);
expect(idx.mroFor('def:C')).toEqual([]);
});
it('records a C3 linearization verbatim (Python diamond)', () => {
// D(B, C) where B(A), C(A). Classical C3: D, B, C, A.
// Our index stores mro excluding self: [B, C, A].
const idx = buildMethodDispatchIndex(
input(['def:D'], { 'def:D': ['def:B', 'def:C', 'def:A'] }),
);
expect(idx.mroFor('def:D')).toEqual(['def:B', 'def:C', 'def:A']);
});
it('records a BFS linearization verbatim (Java-style first-wins)', () => {
// D extends B, C; B extends A; C extends A. BFS: B, C, A.
const idx = buildMethodDispatchIndex(
input(['def:D'], { 'def:D': ['def:B', 'def:C', 'def:A'] }),
);
expect(idx.mroFor('def:D')).toEqual(['def:B', 'def:C', 'def:A']);
});
it('records a Ruby-style kind-aware ancestry verbatim', () => {
// class C prepend P1 prepend P2; include M1 include M2
// ruby-mixin walk order (per callback): [P2, P1, M2, M1]
const idx = buildMethodDispatchIndex(
input(['def:C'], { 'def:C': ['def:P2', 'def:P1', 'def:M2', 'def:M1'] }),
);
expect(idx.mroFor('def:C')).toEqual(['def:P2', 'def:P1', 'def:M2', 'def:M1']);
});
it('records an empty chain for Rust qualified-syntax owners', () => {
// Rust: no auto-MRO; callback returns []
const idx = buildMethodDispatchIndex(input(['def:RustStruct'], { 'def:RustStruct': [] }));
expect(idx.mroFor('def:RustStruct')).toEqual([]);
});
});
describe('implements inversion', () => {
it('inverts a single class → interface mapping', () => {
const idx = buildMethodDispatchIndex(
input(['def:Impl'], { 'def:Impl': [] }, { 'def:Impl': ['def:IFace'] }),
);
expect(idx.implementorsOf('def:IFace')).toEqual(['def:Impl']);
});
it('aggregates multiple classes implementing the same interface', () => {
const idx = buildMethodDispatchIndex(
input(
['def:A', 'def:B', 'def:C'],
{ 'def:A': [], 'def:B': [], 'def:C': [] },
{ 'def:A': ['def:I'], 'def:B': ['def:I'], 'def:C': ['def:J'] },
),
);
expect(idx.implementorsOf('def:I')).toEqual(['def:A', 'def:B']);
expect(idx.implementorsOf('def:J')).toEqual(['def:C']);
});
it('preserves iteration order of owners in each implementors bucket', () => {
const idx = buildMethodDispatchIndex(
input(
['def:Z', 'def:Y', 'def:X'],
{ 'def:Z': [], 'def:Y': [], 'def:X': [] },
{ 'def:Z': ['def:I'], 'def:Y': ['def:I'], 'def:X': ['def:I'] },
),
);
expect(idx.implementorsOf('def:I')).toEqual(['def:Z', 'def:Y', 'def:X']);
});
it('deduplicates repeated (interface, owner) pairs within a single callback call', () => {
// Caller may legally return the same interface twice (e.g., a class that
// both `implements IFace` and inherits from a parent that also does).
const idx = buildMethodDispatchIndex(
input(['def:Impl'], { 'def:Impl': [] }, { 'def:Impl': ['def:I', 'def:I', 'def:I'] }),
);
expect(idx.implementorsOf('def:I')).toEqual(['def:Impl']);
});
it('deduplicates when the same owner is listed in `owners` twice (first-write-wins)', () => {
// First-write-wins parity with sibling indexes; subsequent owner entries
// should not re-invoke callbacks for existing MRO, and should not create
// duplicate implementor entries.
let mroCalls = 0;
const impls: Record<DefId, readonly DefId[]> = { 'def:A': ['def:I'] };
const idx = buildMethodDispatchIndex({
owners: ['def:A', 'def:A'],
computeMro: (_) => {
mroCalls++;
return ['def:B'];
},
implementsOf: (o) => impls[o] ?? [],
});
expect(mroCalls).toBe(1);
expect(idx.mroFor('def:A')).toEqual(['def:B']);
expect(idx.implementorsOf('def:I')).toEqual(['def:A']);
});
});
describe('lookup miss / safety surface', () => {
it('returns a frozen empty array on MRO miss', () => {
const idx = buildMethodDispatchIndex(input(['def:A'], { 'def:A': [] }));
const miss = idx.mroFor('def:Missing');
expect(miss).toEqual([]);
expect(() => (miss as unknown as DefId[]).push('x')).toThrow();
});
it('returns a frozen empty array on implementors miss', () => {
const idx = buildMethodDispatchIndex(input(['def:A'], { 'def:A': [] }));
const miss = idx.implementorsOf('def:Missing');
expect(miss).toEqual([]);
expect(() => (miss as unknown as DefId[]).push('x')).toThrow();
});
it('freezes stored MRO arrays (readonly surface)', () => {
const idx = buildMethodDispatchIndex(input(['def:A'], { 'def:A': ['def:B'] }));
const chain = idx.mroFor('def:A');
expect(() => (chain as unknown as DefId[]).push('x')).toThrow();
});
it('freezes stored implementors arrays (readonly surface)', () => {
const idx = buildMethodDispatchIndex(
input(['def:A'], { 'def:A': [] }, { 'def:A': ['def:I'] }),
);
const impls = idx.implementorsOf('def:I');
expect(() => (impls as unknown as DefId[]).push('x')).toThrow();
});
it('isolates stored MRO from later mutation of the callback-returned array', () => {
const mutable = ['def:B', 'def:C'];
const idx = buildMethodDispatchIndex({
owners: ['def:A'],
computeMro: () => mutable,
implementsOf: () => [],
});
mutable.push('def:D');
expect(idx.mroFor('def:A')).toEqual(['def:B', 'def:C']);
});
});
describe('readonly surface', () => {
it('exposes `mroByOwnerDefId` as a read-only Map for direct iteration', () => {
const idx = buildMethodDispatchIndex(
input(['def:A', 'def:B'], { 'def:A': [], 'def:B': ['def:A'] }),
);
const owners = Array.from(idx.mroByOwnerDefId.keys()).sort();
expect(owners).toEqual(['def:A', 'def:B']);
});
it('exposes `implsByInterfaceDefId` as a read-only Map for direct iteration', () => {
const idx = buildMethodDispatchIndex(
input(
['def:A', 'def:B'],
{ 'def:A': [], 'def:B': [] },
{ 'def:A': ['def:I'], 'def:B': ['def:J'] },
),
);
const keys = Array.from(idx.implsByInterfaceDefId.keys()).sort();
expect(keys).toEqual(['def:I', 'def:J']);
});
});
});