GitNexus/gitnexus/test/unit/scope-resolution/walkers-augmentations.test.ts
Copilot d3d4fa31bb
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
fix(scope-resolution): gate C#/Kotlin free calls by instance ownership (#2563) (#2654)
* Initial plan

* fix(scope-resolution): gate C# and Kotlin free calls

* fix(scope-resolution): keep Kotlin ownership gate safe

* Apply remaining changes

* perf(scope-resolution): benchmark and cache ownership gates

* test(scope-resolution): simplify benchmark scaling loop

* refactor(scope-resolution): encapsulate ownership cache

* test(scope-resolution): enforce subquadratic ownership scaling

* fix(scope-resolution): address ownership review findings

* test(csharp): regenerate capture golden for #2563 fixtures

The committed expected-captures.json was missing the new
NamespaceOwnerCollision.cs entry and carried a stale SameFileCases.cs
digest/count (56 → 67), so csharp-captures-golden.test.ts was the sole
red check on the PR. Regenerate with UPDATE_GOLDEN=1 to match the
fixtures the bench fingerprint already reflects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 13:31:56 +01:00

250 lines
10 KiB
TypeScript

/**
* Unit coverage for `lookupBindingsAt` — the dual-source binding
* lookup primitive used by every walker that needs cross-file
* visibility (Step 2 of the binding-augmentation-channel refactor).
*
* These tests pin the contract exhaustively: precedence (finalized
* first), dedup (by `def.nodeId`), empty-array semantics, and the
* shared-empty-frozen-array identity for misses. Every other walker
* test in this directory delegates to `lookupBindingsAt` after the
* refactor, so a regression here surfaces quickly.
*/
import { describe, it, expect } from 'vitest';
import {
findAllCallableBindingCandidatesInScope,
findCallableBindingInScope,
findClassBindingInScope,
findExportedDefByName,
lookupBindingsAt,
} from '../../../src/core/ingestion/scope-resolution/scope/walkers.js';
import type { BindingRef, Scope, ScopeId, ScopeTree, SymbolDefinition } from 'gitnexus-shared';
import type { ScopeResolutionIndexes } from '../../../src/core/ingestion/model/scope-resolution-indexes.js';
import type { WorkspaceResolutionIndex } from '../../../src/core/ingestion/scope-resolution/workspace-index.js';
const SCOPE = 'scope:m' as ScopeId;
const def = (nodeId: string): SymbolDefinition =>
({ nodeId, filePath: 'm.ts', type: 'Function' }) as SymbolDefinition;
const ref = (nodeId: string, origin: BindingRef['origin'] = 'local'): BindingRef =>
({ def: def(nodeId), origin }) as BindingRef;
function indexesWith({
finalized,
augmented,
workspace,
}: {
finalized?: readonly BindingRef[];
augmented?: readonly BindingRef[];
workspace?: readonly BindingRef[];
}): ScopeResolutionIndexes {
const bindings = new Map<ScopeId, Map<string, readonly BindingRef[]>>();
if (finalized !== undefined) {
Object.freeze(finalized as BindingRef[]);
bindings.set(SCOPE, new Map([['name', finalized]]));
}
const bindingAugmentations = new Map<ScopeId, Map<string, readonly BindingRef[]>>();
if (augmented !== undefined) bindingAugmentations.set(SCOPE, new Map([['name', augmented]]));
const workspaceFqnBindings = new Map<string, readonly BindingRef[]>();
if (workspace !== undefined) workspaceFqnBindings.set('name', workspace);
return {
bindings,
bindingAugmentations,
workspaceFqnBindings,
} as unknown as ScopeResolutionIndexes;
}
function scope(id: ScopeId, bindings = new Map<string, readonly BindingRef[]>()): Scope {
return {
id,
kind: 'Module',
parent: null,
filePath: 'm.ts',
range: { startLine: 1, startColumn: 0, endLine: 1, endColumn: 0 },
bindings,
imports: [],
ownedDefs: [],
typeBindings: new Map(),
} as unknown as Scope;
}
function indexesForScopeLookup(
moduleScope: Scope,
augmented: Map<string, readonly BindingRef[]>,
): ScopeResolutionIndexes {
const scopeTree = {
getScope: (id: ScopeId) => (id === moduleScope.id ? moduleScope : undefined),
} as unknown as ScopeTree;
return {
scopeTree,
bindings: new Map(),
bindingAugmentations: new Map([[moduleScope.id, augmented]]),
} as unknown as ScopeResolutionIndexes;
}
describe('lookupBindingsAt', () => {
it('returns the finalized bucket when augmentations are absent', () => {
const finalized = [ref('A'), ref('B')];
const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized }));
expect(out).toEqual(finalized);
// Identity preserved when only one channel populates — no allocation.
expect(out).toBe(finalized);
});
it('returns the augmented bucket when finalized is absent', () => {
const augmented = [ref('X', 'namespace')];
const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ augmented }));
expect(out).toEqual(augmented);
expect(out).toBe(augmented);
});
it('concatenates with finalized first when both populate disjoint nodeIds', () => {
const finalized = [ref('A', 'import'), ref('B', 'import')];
const augmented = [ref('C', 'namespace'), ref('D', 'namespace')];
const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized, augmented }));
expect(out.map((b) => b.def.nodeId)).toEqual(['A', 'B', 'C', 'D']);
});
it('dedupes augmented entries that share a nodeId with finalized (finalized wins)', () => {
const finalized = [ref('A', 'import'), ref('B', 'import')];
const augmented = [ref('A', 'namespace'), ref('C', 'namespace')];
const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized, augmented }));
expect(out.map((b) => b.def.nodeId)).toEqual(['A', 'B', 'C']);
expect(out.find((b) => b.def.nodeId === 'A')!.origin).toBe('import');
});
// Third channel: workspaceFqnBindings (scope-independent — global-namespace
// C# types / PHP FQNs). Consulted LAST, after finalized + augmented.
it('returns the workspace bucket when it is the only channel', () => {
const workspace = [ref('W', 'namespace')];
const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ workspace }));
expect(out).toEqual(workspace);
expect(out).toBe(workspace); // identity preserved when only one channel populates
});
it('appends workspace entries after finalized and augmented', () => {
const finalized = [ref('A', 'import')];
const augmented = [ref('B', 'namespace')];
const workspace = [ref('C', 'namespace')];
const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized, augmented, workspace }));
expect(out.map((b) => b.def.nodeId)).toEqual(['A', 'B', 'C']);
});
it('dedupes workspace entries already present in finalized/augmented (workspace loses)', () => {
const finalized = [ref('A', 'import')];
const augmented = [ref('B', 'namespace')];
const workspace = [ref('A', 'namespace'), ref('B', 'namespace'), ref('C', 'namespace')];
const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized, augmented, workspace }));
expect(out.map((b) => b.def.nodeId)).toEqual(['A', 'B', 'C']);
// The surviving A/B keep their finalized/augmented identity, not workspace's.
expect(out.find((b) => b.def.nodeId === 'A')!.origin).toBe('import');
});
it('keeps finalized metadata when the same nodeId appears in both channels', () => {
const finalizedDef = {
nodeId: 'A',
filePath: 'finalized.ts',
qualifiedName: 'finalized.A',
type: 'Function',
} as SymbolDefinition;
const augmentedDef = {
nodeId: 'A',
filePath: 'augmented.ts',
qualifiedName: 'augmented.A',
type: 'Method',
} as SymbolDefinition;
const out = lookupBindingsAt(
SCOPE,
'name',
indexesWith({
finalized: [{ def: finalizedDef, origin: 'import' } as BindingRef],
augmented: [{ def: augmentedDef, origin: 'namespace' } as BindingRef],
}),
);
expect(out).toHaveLength(1);
expect(out[0]!.def.filePath).toBe('finalized.ts');
expect(out[0]!.def.qualifiedName).toBe('finalized.A');
expect(out[0]!.origin).toBe('import');
});
it('returns the shared empty array on a miss in both channels', () => {
const a = lookupBindingsAt(SCOPE, 'name', indexesWith({}));
const b = lookupBindingsAt(SCOPE, 'other', indexesWith({}));
expect(a).toEqual([]);
expect(b).toEqual([]);
expect(a).toBe(b);
expect(Object.isFrozen(a)).toBe(true);
});
it('treats an empty finalized bucket as absent (returns augmented)', () => {
const augmented = [ref('Z', 'namespace')];
const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized: [], augmented }));
expect(out).toBe(augmented);
});
it('treats an empty augmented bucket as absent (returns finalized)', () => {
const finalized = [ref('Z', 'import')];
const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized, augmented: [] }));
expect(out).toBe(finalized);
});
it('returns the shared empty array when both buckets exist but are empty', () => {
const out = lookupBindingsAt(SCOPE, 'name', indexesWith({ finalized: [], augmented: [] }));
expect(out).toEqual([]);
expect(Object.isFrozen(out)).toBe(true);
});
});
describe('walker helpers read bindingAugmentations', () => {
it('findClassBindingInScope finds class-like refs that exist only in augmentations', () => {
const moduleScope = scope(SCOPE);
const classRef = {
def: { ...def('ClassA'), type: 'Class' },
origin: 'namespace',
} as BindingRef;
const indexes = indexesForScopeLookup(moduleScope, new Map([['ClassA', [classRef]]]));
expect(findClassBindingInScope(SCOPE, 'ClassA', indexes)?.nodeId).toBe('ClassA');
});
it('findCallableBindingInScope finds callable refs that exist only in augmentations', () => {
const moduleScope = scope(SCOPE);
const callableRef = { def: def('callMe'), origin: 'import' } as BindingRef;
const indexes = indexesForScopeLookup(moduleScope, new Map([['callMe', [callableRef]]]));
expect(findCallableBindingInScope(SCOPE, 'callMe', indexes)?.nodeId).toBe('callMe');
});
it('preserves augmentation provenance masked by a finalized binding', () => {
const moduleScope = scope(SCOPE);
const callable = def('callMe');
const finalized = { def: callable, origin: 'local' } as BindingRef;
const staticImport = {
def: callable,
origin: 'import',
visibility: 'static-member-import',
} as BindingRef;
const indexes = indexesForScopeLookup(moduleScope, new Map([['callMe', [staticImport]]]));
indexes.bindings.set(SCOPE, new Map([['callMe', [finalized]]]));
const candidates = findAllCallableBindingCandidatesInScope(SCOPE, 'callMe', indexes);
expect(candidates).toHaveLength(1);
expect(candidates[0]!.def).toBe(callable);
expect(candidates[0]!.bindings).toEqual([finalized, staticImport]);
});
it('findExportedDefByName finds callable refs that exist only in augmentations', () => {
const moduleScope = scope(SCOPE);
const callableRef = { def: def('fromAugmentation'), origin: 'import' } as BindingRef;
const indexes = indexesForScopeLookup(moduleScope, new Map([['run', [callableRef]]]));
const workspaceIndex = {
moduleScopeByFile: new Map(),
} as unknown as WorkspaceResolutionIndex;
expect(findExportedDefByName('run', SCOPE, indexes, workspaceIndex)?.nodeId).toBe(
'fromAugmentation',
);
});
});