mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-01 02:01:24 +00:00
* 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>
383 lines
15 KiB
TypeScript
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);
|
|
});
|
|
});
|