GitNexus/gitnexus/test/unit/model/method-registry.test.ts
Gergő Magyar 52924ef12c
perf(emit): drop six equivalent redundancies on Java-scale index path (#3129)
* perf(scope): reuse deferred sites and signatures for callable-value-flow

Pass the Phase 4 deferred-site collection and signature map into emitCallableValueFlow so the emit path does not rescan the same call sites.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(ingestion): nest owner registries instead of composite string keys

Look up methods, fields, and nested types via Map<owner, Map<name, defs>> while keeping EMPTY identity and TypeRegistry miss [] semantics.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(scope): memoize resolveDefGraphId per nodeLookup

Cache graph ids on a WeakMap keyed by lookup identity so heritage rebuild invalidates, with an env opt-out that skips the cache.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(csv): prepare file content once and LRU-touch via Map order

Cache split lines and binary flags per source file so snippet and FTS extraction do not re-scan the same bytes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(identity): allow in-process cache guards only when opted in or unwritable

Keep the ≥128-guard subprocess default on writable installs; use direct snapshots when the env flag is set or W_OK fails on the analyzer tree.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: share empty CSV payload and restore spacing after memo key (U4, U3)

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(scope): assert resolveDefGraphId memo skips a second lookup walk

The ID-equality checks still passed with the memo disabled. Count Map#get on the lookup so a repeat call must hit the WeakMap cache. Also apply prettier on the remaining emit-path files CI flagged.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test: pin identity W_OK probes and memo env isolation

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: document emit-path env knobs and fix registry headers

CONTRIBUTING requires README rows for new GITNEXUS_* variables; also drop stale owner\0name comments after the nested-map change.

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 11:22:55 +00:00

383 lines
15 KiB
TypeScript

/**
* Unit tests for MethodRegistry (SM-20).
*
* MethodRegistry is the most complex of the three owner-scoped registries
* because it supports C++/Java/C# overloads. Lookup does two layers of
* narrowing after the primary `ownerNodeId + methodName` key match:
*
* 1. Arity filter: when `argCount` is provided and there are multiple
* overloads, keep only those whose parameterCount range can match.
* Variadic candidates (`parameterCount === undefined`) are retained.
* If arity excludes EVERY candidate, fall back to the full pool so
* fuzzy resolution still has something to work with (the "arity
* fallback" branch — flagged as an untested branch by the testing
* reviewer).
*
* 2. Return-type dedup: among the remaining candidates, if every def
* shares the same defined returnType, return the first. If return
* types differ, return undefined (truly ambiguous).
*/
import { describe, it, expect } from 'vitest';
import { createMethodRegistry } from '../../../src/core/ingestion/model/method-registry.js';
import { makeMethod } from './helpers.js';
describe('MethodRegistry — basic lookup', () => {
it('returns undefined when the registry is empty', () => {
const reg = createMethodRegistry();
expect(reg.lookupMethodByOwner('class:User', 'save')).toBeUndefined();
});
it('register + lookup round-trips the def reference', () => {
const reg = createMethodRegistry();
const def = makeMethod({ nodeId: 'method:User.save' });
reg.register('class:User', 'save', def);
expect(reg.lookupMethodByOwner('class:User', 'save')).toBe(def);
});
it('isolates methods by ownerNodeId — same method name on two classes does not collide', () => {
const reg = createMethodRegistry();
const userSave = makeMethod({ nodeId: 'method:User.save' });
const orderSave = makeMethod({ nodeId: 'method:Order.save' });
reg.register('class:User', 'save', userSave);
reg.register('class:Order', 'save', orderSave);
expect(reg.lookupMethodByOwner('class:User', 'save')?.nodeId).toBe('method:User.save');
expect(reg.lookupMethodByOwner('class:Order', 'save')?.nodeId).toBe('method:Order.save');
});
});
describe('MethodRegistry — arity narrowing', () => {
it('narrows overloads by parameterCount when argCount is provided', () => {
const reg = createMethodRegistry();
const greetEmpty = makeMethod({ nodeId: 'method:greet#0', parameterCount: 0 });
const greetString = makeMethod({
nodeId: 'method:greet#1',
parameterCount: 1,
returnType: 'void',
});
reg.register('class:User', 'greet', greetEmpty);
reg.register('class:User', 'greet', greetString);
// argCount 0 matches only the 0-arg overload
expect(reg.lookupMethodByOwner('class:User', 'greet', 0)?.nodeId).toBe('method:greet#0');
// argCount 1 matches only the 1-arg overload
expect(reg.lookupMethodByOwner('class:User', 'greet', 1)?.nodeId).toBe('method:greet#1');
});
it('arity fallback — when no overload matches argCount, returns from the full pool (testing reviewer T-01)', () => {
// This is the explicit arity-fallback branch flagged as untested.
// Without the fallback, `save(1)` / `save(2)` with argCount=3 would
// return undefined. With the fallback, it returns one of them so the
// caller's fuzzy resolution path can still make progress.
const reg = createMethodRegistry();
const save1 = makeMethod({ nodeId: 'method:save#1', parameterCount: 1, returnType: 'void' });
const save2 = makeMethod({ nodeId: 'method:save#2', parameterCount: 2, returnType: 'void' });
reg.register('class:User', 'save', save1);
reg.register('class:User', 'save', save2);
// argCount 3 matches neither; fallback returns one of them (first
// wins because both share the same returnType 'void').
const result = reg.lookupMethodByOwner('class:User', 'save', 3);
expect(result).toBeDefined();
expect(result?.nodeId).toBe('method:save#1');
});
it('requiredParameterCount range — argCount between required and total is accepted (testing reviewer T-02)', () => {
// Default parameters: `bar(a, b=1, c=2)` has requiredParameterCount: 1,
// parameterCount: 3. Calls with argCount 1, 2, and 3 must all match.
const reg = createMethodRegistry();
const bar = makeMethod({
nodeId: 'method:bar',
parameterCount: 3,
requiredParameterCount: 1,
returnType: 'int',
});
// Add a second overload so arity filtering engages (defs.length > 1).
const barOther = makeMethod({
nodeId: 'method:bar#other',
parameterCount: 5,
requiredParameterCount: 5,
returnType: 'int',
});
reg.register('class:Calc', 'bar', bar);
reg.register('class:Calc', 'bar', barOther);
expect(reg.lookupMethodByOwner('class:Calc', 'bar', 1)?.nodeId).toBe('method:bar');
expect(reg.lookupMethodByOwner('class:Calc', 'bar', 2)?.nodeId).toBe('method:bar');
expect(reg.lookupMethodByOwner('class:Calc', 'bar', 3)?.nodeId).toBe('method:bar');
// argCount 5 matches the second overload only
expect(reg.lookupMethodByOwner('class:Calc', 'bar', 5)?.nodeId).toBe('method:bar#other');
});
it('variadic fallback — defs with parameterCount=undefined are retained during arity narrowing', () => {
const reg = createMethodRegistry();
const fixed = makeMethod({
nodeId: 'method:print#fixed',
parameterCount: 1,
returnType: 'void',
});
const variadic = makeMethod({
nodeId: 'method:print#variadic',
parameterCount: undefined,
returnType: 'void',
});
reg.register('class:Logger', 'print', fixed);
reg.register('class:Logger', 'print', variadic);
// argCount 5 excludes fixed (5 > parameterCount 1) but retains
// variadic (parameterCount=undefined bypasses the range check).
// Result: variadic is the only surviving candidate.
const result = reg.lookupMethodByOwner('class:Logger', 'print', 5);
expect(result?.nodeId).toBe('method:print#variadic');
});
it('variadic + matching fixed — argCount in fixed range keeps both, first wins on shared returnType', () => {
const reg = createMethodRegistry();
const fixed = makeMethod({
nodeId: 'method:print#fixed',
parameterCount: 2,
returnType: 'void',
});
const variadic = makeMethod({
nodeId: 'method:print#variadic',
parameterCount: undefined,
returnType: 'void',
});
reg.register('class:Logger', 'print', fixed);
reg.register('class:Logger', 'print', variadic);
// argCount 2 satisfies fixed's range AND keeps variadic.
// Both share returnType 'void', so first-registered wins.
const result = reg.lookupMethodByOwner('class:Logger', 'print', 2);
expect(result?.nodeId).toBe('method:print#fixed');
});
});
describe('MethodRegistry — return-type dedup', () => {
it('returns first when all overloads share the same returnType', () => {
const reg = createMethodRegistry();
const a = makeMethod({ nodeId: 'method:a', parameterCount: 1, returnType: 'int' });
const b = makeMethod({ nodeId: 'method:b', parameterCount: 1, returnType: 'int' });
reg.register('class:X', 'compute', a);
reg.register('class:X', 'compute', b);
// Two overloads with same arity & same returnType → first wins
expect(reg.lookupMethodByOwner('class:X', 'compute', 1)?.nodeId).toBe('method:a');
});
it('returns undefined when overloads differ in returnType (truly ambiguous)', () => {
const reg = createMethodRegistry();
const intVersion = makeMethod({
nodeId: 'method:int',
parameterCount: 1,
returnType: 'int',
});
const stringVersion = makeMethod({
nodeId: 'method:string',
parameterCount: 1,
returnType: 'string',
});
reg.register('class:X', 'compute', intVersion);
reg.register('class:X', 'compute', stringVersion);
// Same arity, different returnType → undefined (ambiguous)
expect(reg.lookupMethodByOwner('class:X', 'compute', 1)).toBeUndefined();
});
it('returns undefined when firstReturnType is itself undefined', () => {
const reg = createMethodRegistry();
const a = makeMethod({ nodeId: 'method:a', parameterCount: 1, returnType: undefined });
const b = makeMethod({ nodeId: 'method:b', parameterCount: 1, returnType: 'int' });
reg.register('class:X', 'compute', a);
reg.register('class:X', 'compute', b);
// First def has no declared returnType → bail out as undefined
expect(reg.lookupMethodByOwner('class:X', 'compute', 1)).toBeUndefined();
});
it('single-overload methods skip the dedup path', () => {
const reg = createMethodRegistry();
reg.register(
'class:X',
'only',
makeMethod({ nodeId: 'method:only', parameterCount: 1, returnType: undefined }),
);
// Only one candidate → returned directly regardless of returnType
expect(reg.lookupMethodByOwner('class:X', 'only', 1)?.nodeId).toBe('method:only');
});
});
describe('MethodRegistry — clear()', () => {
it('empties the registry', () => {
const reg = createMethodRegistry();
reg.register('class:User', 'save', makeMethod());
reg.register('class:Order', 'update', makeMethod());
reg.clear();
expect(reg.lookupMethodByOwner('class:User', 'save')).toBeUndefined();
expect(reg.lookupMethodByOwner('class:Order', 'update')).toBeUndefined();
});
it('allows re-registration after clear', () => {
const reg = createMethodRegistry();
reg.register('class:User', 'save', makeMethod({ nodeId: 'method:first' }));
reg.clear();
reg.register('class:User', 'save', makeMethod({ nodeId: 'method:second' }));
expect(reg.lookupMethodByOwner('class:User', 'save')?.nodeId).toBe('method:second');
});
});
// ---------------------------------------------------------------------------
// lookupMethodByName — flat-by-name secondary index (A4 / plan 006)
// ---------------------------------------------------------------------------
describe('MethodRegistry — lookupMethodByName', () => {
it('returns an empty array when no method with that name is registered', () => {
const reg = createMethodRegistry();
expect(reg.lookupMethodByName('save')).toEqual([]);
});
it('returns a singleton array after one registration', () => {
const reg = createMethodRegistry();
const def = makeMethod({ nodeId: 'method:User.save' });
reg.register('class:User', 'save', def);
const result = reg.lookupMethodByName('save');
expect(result).toHaveLength(1);
expect(result[0]).toBe(def);
});
it('accumulates homonym registrations across different owners in order', () => {
const reg = createMethodRegistry();
const userSave = makeMethod({ nodeId: 'method:User.save' });
const orderSave = makeMethod({ nodeId: 'method:Order.save' });
reg.register('class:User', 'save', userSave);
reg.register('class:Order', 'save', orderSave);
const result = reg.lookupMethodByName('save');
expect(result).toHaveLength(2);
expect(result).toEqual([userSave, orderSave]);
});
it('accumulates overloads under the same owner', () => {
const reg = createMethodRegistry();
const overload1 = makeMethod({ nodeId: 'method:User.save#0', parameterCount: 0 });
const overload2 = makeMethod({ nodeId: 'method:User.save#1', parameterCount: 1 });
reg.register('class:User', 'save', overload1);
reg.register('class:User', 'save', overload2);
const result = reg.lookupMethodByName('save');
expect(result).toHaveLength(2);
expect(result).toEqual([overload1, overload2]);
});
it('returns an empty array after clear()', () => {
const reg = createMethodRegistry();
reg.register('class:User', 'save', makeMethod({ nodeId: 'method:old' }));
reg.clear();
expect(reg.lookupMethodByName('save')).toEqual([]);
});
it('re-registering after clear only returns post-clear defs', () => {
const reg = createMethodRegistry();
reg.register('class:User', 'save', makeMethod({ nodeId: 'method:old' }));
reg.clear();
const fresh = makeMethod({ nodeId: 'method:fresh' });
reg.register('class:User', 'save', fresh);
const result = reg.lookupMethodByName('save');
expect(result).toHaveLength(1);
expect(result[0]).toBe(fresh);
});
it('returns the same SymbolDefinition reference as lookupMethodByOwner (dual-index identity)', () => {
const reg = createMethodRegistry();
const def = makeMethod({ nodeId: 'method:User.save' });
reg.register('class:User', 'save', def);
const byOwner = reg.lookupMethodByOwner('class:User', 'save');
const byName = reg.lookupMethodByName('save');
expect(byName).toHaveLength(1);
expect(Object.is(byName[0], byOwner)).toBe(true);
});
it('does not return methods with different names', () => {
const reg = createMethodRegistry();
reg.register('class:User', 'save', makeMethod({ nodeId: 'method:User.save' }));
reg.register('class:User', 'load', makeMethod({ nodeId: 'method:User.load' }));
expect(reg.lookupMethodByName('save')).toHaveLength(1);
expect(reg.lookupMethodByName('load')).toHaveLength(1);
expect(reg.lookupMethodByName('missing')).toEqual([]);
});
});
describe('hasFunctionMethods flag', () => {
it('is false for a fresh registry', () => {
const reg = createMethodRegistry();
expect(reg.hasFunctionMethods).toBe(false);
});
it('stays false after registering only strict-Method defs', () => {
const reg = createMethodRegistry();
reg.register('class:User', 'save', makeMethod({ nodeId: 'method:User.save', type: 'Method' }));
reg.register(
'class:User',
'load',
makeMethod({ nodeId: 'method:User.load', type: 'Constructor' }),
);
expect(reg.hasFunctionMethods).toBe(false);
});
it('flips to true when a Function-typed def (Python/Rust/Kotlin class method) is registered', () => {
const reg = createMethodRegistry();
reg.register('class:User', 'greet', makeMethod({ nodeId: 'fn:User.greet', type: 'Function' }));
expect(reg.hasFunctionMethods).toBe(true);
});
it('stays true after further strict-Method registrations', () => {
const reg = createMethodRegistry();
reg.register('class:User', 'greet', makeMethod({ nodeId: 'fn:User.greet', type: 'Function' }));
reg.register('class:Dog', 'bark', makeMethod({ nodeId: 'method:Dog.bark', type: 'Method' }));
expect(reg.hasFunctionMethods).toBe(true);
});
it('resets to false after clear()', () => {
const reg = createMethodRegistry();
reg.register('class:User', 'greet', makeMethod({ nodeId: 'fn:User.greet', type: 'Function' }));
expect(reg.hasFunctionMethods).toBe(true);
reg.clear();
expect(reg.hasFunctionMethods).toBe(false);
});
});
describe('MethodRegistry — EMPTY identity', () => {
it('returns the same frozen empty array on miss', () => {
const reg = createMethodRegistry();
expect(reg.lookupMethodByName('missing')).toBe(reg.lookupMethodByName('other'));
expect(reg.lookupAllByOwner('class:Nope', 'x')).toBe(reg.lookupAllByOwner('class:Nope', 'y'));
expect(Object.isFrozen(reg.lookupAllByOwner('class:Nope', 'x'))).toBe(true);
});
});