mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
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.
216 lines
8.7 KiB
TypeScript
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']);
|
|
});
|
|
});
|
|
});
|