mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
732 lines
25 KiB
TypeScript
732 lines
25 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
buildDefIndex,
|
|
buildMethodDispatchIndex,
|
|
buildModuleScopeIndex,
|
|
buildQualifiedNameIndex,
|
|
buildScopeTree,
|
|
type BindingRef,
|
|
type ImportEdge,
|
|
type Range,
|
|
type ReferenceSite,
|
|
type Scope,
|
|
type ScopeId,
|
|
type SymbolDefinition,
|
|
type TypeRef,
|
|
} from 'gitnexus-shared';
|
|
import { resolveReferenceSites } from '../../../src/core/ingestion/resolve-references.js';
|
|
import type { ScopeResolutionIndexes } from '../../../src/core/ingestion/model/scope-resolution-indexes.js';
|
|
import { createKnowledgeGraph } from '../../../src/core/graph/graph.js';
|
|
import { createSemanticModel } from '../../../src/core/ingestion/model/semantic-model.js';
|
|
import { buildGraphNodeLookup } from '../../../src/core/ingestion/scope-resolution/graph-bridge/node-lookup.js';
|
|
import { emitReceiverBoundCalls } from '../../../src/core/ingestion/scope-resolution/passes/receiver-bound-calls.js';
|
|
import { resolveCompoundReceiverClass } from '../../../src/core/ingestion/scope-resolution/passes/compound-receiver.js';
|
|
import { buildWorkspaceResolutionIndex } from '../../../src/core/ingestion/scope-resolution/workspace-index.js';
|
|
import type { SourceBindingResolution } from '../../../src/core/ingestion/scope-resolution/source-binding-resolution.js';
|
|
|
|
const range = (sl = 1, sc = 0, el = 100, ec = 0): Range => ({
|
|
startLine: sl,
|
|
startCol: sc,
|
|
endLine: el,
|
|
endCol: ec,
|
|
});
|
|
|
|
const mkDef = (overrides: Partial<SymbolDefinition> & { nodeId: string }): SymbolDefinition => ({
|
|
nodeId: overrides.nodeId,
|
|
filePath: overrides.filePath ?? 'x.ts',
|
|
type: overrides.type ?? 'Class',
|
|
...overrides,
|
|
});
|
|
|
|
const mkScope = (input: {
|
|
id: ScopeId;
|
|
parent: ScopeId | null;
|
|
kind?: Scope['kind'];
|
|
filePath?: string;
|
|
range?: Range;
|
|
bindings?: Record<string, readonly BindingRef[]>;
|
|
typeBindings?: Record<string, TypeRef>;
|
|
ownedDefs?: readonly SymbolDefinition[];
|
|
}): Scope => ({
|
|
id: input.id,
|
|
parent: input.parent,
|
|
kind: input.kind ?? 'Module',
|
|
filePath: input.filePath ?? 'x.ts',
|
|
range: input.range ?? range(),
|
|
bindings: new Map(Object.entries(input.bindings ?? {})),
|
|
imports: [],
|
|
typeBindings: new Map(Object.entries(input.typeBindings ?? {})),
|
|
ownedDefs: input.ownedDefs ?? [],
|
|
});
|
|
|
|
const typeRef = (rawName: string, declaredAtScope: ScopeId): TypeRef => ({
|
|
rawName,
|
|
declaredAtScope,
|
|
source: 'parameter-annotation',
|
|
});
|
|
|
|
function makeIndexes(
|
|
scopes: Scope[],
|
|
defs: SymbolDefinition[],
|
|
referenceSites: readonly ReferenceSite[],
|
|
mro: Record<string, readonly string[]> = {},
|
|
): ScopeResolutionIndexes {
|
|
return {
|
|
scopeTree: buildScopeTree(scopes),
|
|
defs: buildDefIndex(defs),
|
|
qualifiedNames: buildQualifiedNameIndex(defs),
|
|
moduleScopes: buildModuleScopeIndex(
|
|
scopes
|
|
.filter((scope) => scope.kind === 'Module')
|
|
.map((scope) => ({ filePath: scope.filePath, moduleScopeId: scope.id })),
|
|
),
|
|
methodDispatch: buildMethodDispatchIndex({
|
|
owners: Array.from(new Set(defs.map((def) => def.nodeId))),
|
|
computeMro: (owner) => mro[owner] ?? [],
|
|
implementsOf: () => [],
|
|
}),
|
|
imports: new Map(),
|
|
bindings: new Map(),
|
|
workspaceFqnBindings: new Map(),
|
|
workspaceTypeBindings: new Map(),
|
|
namespaceFqnBindings: new Map(),
|
|
namespaceTypeBindings: new Map(),
|
|
accessibleNamespacesByScope: new Map(),
|
|
bindingAugmentations: new Map(),
|
|
referenceSites,
|
|
sccs: [],
|
|
stats: {
|
|
totalFiles: 0,
|
|
totalEdges: 0,
|
|
linkedEdges: 0,
|
|
unresolvedEdges: 0,
|
|
sccCount: 0,
|
|
largestSccSize: 0,
|
|
ambiguousWildcardExports: [],
|
|
},
|
|
};
|
|
}
|
|
|
|
describe('resolveReferenceSites', () => {
|
|
it('resolves a written qualified type without falling back to an unrelated leaf', () => {
|
|
const definitions = ['Outer', 'Other'].map((owner) =>
|
|
mkDef({ nodeId: `${owner}.Target`, type: 'Class', qualifiedName: `${owner}.Target` }),
|
|
);
|
|
const module = mkScope({
|
|
id: 'module',
|
|
parent: null,
|
|
bindings: { Target: [{ def: definitions[1]!, origin: 'local' }] },
|
|
});
|
|
const exact: ReferenceSite = {
|
|
name: 'Target',
|
|
rawQualifiedName: 'Outer::Target',
|
|
kind: 'type-reference',
|
|
inScope: module.id,
|
|
atRange: range(3, 0, 3, 13),
|
|
};
|
|
const absent: ReferenceSite = {
|
|
...exact,
|
|
rawQualifiedName: 'Missing::Target',
|
|
atRange: range(4, 0, 4, 15),
|
|
};
|
|
const bindings = new Map<ReferenceSite, SourceBindingResolution>();
|
|
const result = resolveReferenceSites({
|
|
scopes: makeIndexes([module], definitions, [exact, absent]),
|
|
ownedMembersByOwner: () => [],
|
|
recordSourceBinding: (site, binding) => bindings.set(site, binding),
|
|
});
|
|
expect(
|
|
result.referenceIndex.bySourceScope.get(module.id)?.map((reference) => reference.toDef),
|
|
).toEqual([definitions[0]!.nodeId]);
|
|
expect(bindings.get(exact)).toMatchObject({
|
|
status: 'resolved',
|
|
targetDefIds: [definitions[0]!.nodeId],
|
|
});
|
|
expect(bindings.get(absent)).toMatchObject({ status: 'unresolved', targetDefIds: [] });
|
|
});
|
|
|
|
it('honors a shadowed or imported qualifier before considering a global qualified type', () => {
|
|
const decoy = mkDef({
|
|
nodeId: 'decoy.Target',
|
|
type: 'Class',
|
|
qualifiedName: 'Outer.Target',
|
|
filePath: 'decoy.ts',
|
|
});
|
|
const actual = mkDef({
|
|
nodeId: 'source.Target',
|
|
type: 'Class',
|
|
qualifiedName: 'Target',
|
|
filePath: 'source.ts',
|
|
});
|
|
const module = mkScope({ id: 'module', parent: null });
|
|
const inner: Scope = {
|
|
...mkScope({ id: 'inner', parent: module.id, kind: 'Function' }),
|
|
lexicalNames: new Set(['Outer']),
|
|
};
|
|
const imported: ReferenceSite = {
|
|
name: 'Target',
|
|
rawQualifiedName: 'Outer.Target',
|
|
kind: 'type-reference',
|
|
inScope: module.id,
|
|
atRange: range(3, 0, 3, 12),
|
|
};
|
|
const shadowed: ReferenceSite = { ...imported, inScope: inner.id, atRange: range(5, 0, 5, 12) };
|
|
const imports = new Map<ScopeId, readonly ImportEdge[]>();
|
|
const indexes = {
|
|
...makeIndexes([module, inner], [decoy, actual], [imported, shadowed]),
|
|
imports,
|
|
};
|
|
imports.set(module.id, [
|
|
{
|
|
kind: 'namespace',
|
|
localName: 'Outer',
|
|
targetExportedName: '*',
|
|
targetFile: actual.filePath,
|
|
},
|
|
]);
|
|
const bindings = new Map<ReferenceSite, SourceBindingResolution>();
|
|
const result = resolveReferenceSites({
|
|
scopes: indexes,
|
|
ownedMembersByOwner: () => [],
|
|
recordSourceBinding: (site, binding) => bindings.set(site, binding),
|
|
});
|
|
expect(
|
|
result.referenceIndex.bySourceScope.get(module.id)?.map((reference) => reference.toDef),
|
|
).toEqual([actual.nodeId]);
|
|
expect(result.referenceIndex.bySourceScope.get(inner.id)).toBeUndefined();
|
|
expect(bindings.get(shadowed)).toMatchObject({ status: 'blocked', targetDefIds: [] });
|
|
});
|
|
|
|
it('drops a type reached through a namespace import when the provider refuses that member', () => {
|
|
const hidden = mkDef({
|
|
nodeId: 'source.Hidden',
|
|
type: 'Class',
|
|
qualifiedName: 'Hidden',
|
|
filePath: 'source.ts',
|
|
});
|
|
const shown = mkDef({
|
|
nodeId: 'source.Shown',
|
|
type: 'Class',
|
|
qualifiedName: 'Shown',
|
|
filePath: 'source.ts',
|
|
});
|
|
const module = mkScope({ id: 'module', parent: null });
|
|
const hiddenSite: ReferenceSite = {
|
|
name: 'Hidden',
|
|
rawQualifiedName: 'ns.Hidden',
|
|
kind: 'type-reference',
|
|
inScope: module.id,
|
|
atRange: range(3, 0, 3, 9),
|
|
};
|
|
const shownSite: ReferenceSite = {
|
|
...hiddenSite,
|
|
name: 'Shown',
|
|
rawQualifiedName: 'ns.Shown',
|
|
atRange: range(4, 0, 4, 8),
|
|
};
|
|
const resolve = (isNamespaceMemberVisible?: (def: SymbolDefinition) => boolean) => {
|
|
const imports = new Map<ScopeId, readonly ImportEdge[]>();
|
|
imports.set(module.id, [
|
|
{ kind: 'namespace', localName: 'ns', targetExportedName: '*', targetFile: 'source.ts' },
|
|
]);
|
|
const bindings = new Map<ReferenceSite, SourceBindingResolution>();
|
|
resolveReferenceSites({
|
|
scopes: {
|
|
...makeIndexes([module], [hidden, shown], [hiddenSite, shownSite]),
|
|
imports,
|
|
},
|
|
ownedMembersByOwner: () => [],
|
|
isNamespaceMemberVisible,
|
|
recordSourceBinding: (site, binding) => bindings.set(site, binding),
|
|
});
|
|
return bindings;
|
|
};
|
|
|
|
// Default: no hook, every declaration of the namespace's target file is a member.
|
|
const unrestricted = resolve();
|
|
expect(unrestricted.get(hiddenSite)).toMatchObject({
|
|
status: 'resolved',
|
|
targetDefIds: [hidden.nodeId],
|
|
});
|
|
expect(unrestricted.get(shownSite)).toMatchObject({
|
|
status: 'resolved',
|
|
targetDefIds: [shown.nodeId],
|
|
});
|
|
|
|
// With a hook: the refused member resolves to nothing; its sibling is untouched.
|
|
const restricted = resolve((member) => member.nodeId !== hidden.nodeId);
|
|
expect(restricted.get(hiddenSite)).toMatchObject({ status: 'unresolved', targetDefIds: [] });
|
|
expect(restricted.get(shownSite)).toMatchObject({
|
|
status: 'resolved',
|
|
targetDefIds: [shown.nodeId],
|
|
});
|
|
});
|
|
|
|
it('does not mistake a nearer class qualifier for a globally spelled owner', () => {
|
|
const decoy = mkDef({ nodeId: 'Outer.Target', type: 'Class', qualifiedName: 'Outer.Target' });
|
|
const local = mkDef({ nodeId: 'Local.Outer', type: 'Class', qualifiedName: 'Local.Outer' });
|
|
const module = mkScope({ id: 'module', parent: null });
|
|
const inner = mkScope({
|
|
id: 'inner',
|
|
parent: module.id,
|
|
kind: 'Function',
|
|
bindings: { Outer: [{ def: local, origin: 'local' }] },
|
|
});
|
|
const site: ReferenceSite = {
|
|
name: 'Target',
|
|
rawQualifiedName: 'Outer::Target',
|
|
kind: 'type-reference',
|
|
inScope: inner.id,
|
|
atRange: range(3, 0, 3, 13),
|
|
};
|
|
const bindings = new Map<ReferenceSite, SourceBindingResolution>();
|
|
const result = resolveReferenceSites({
|
|
scopes: makeIndexes([module, inner], [decoy, local], [site]),
|
|
ownedMembersByOwner: () => [],
|
|
recordSourceBinding: (reference, binding) => bindings.set(reference, binding),
|
|
});
|
|
expect(result.referenceIndex.bySourceScope.get(inner.id)).toBeUndefined();
|
|
expect(bindings.get(site)).toMatchObject({ status: 'unresolved', targetDefIds: [] });
|
|
});
|
|
|
|
it('resolves finalized imported type bindings and preserves nearer lexical shadows', () => {
|
|
const definition = mkDef({
|
|
nodeId: 'source.Target',
|
|
type: 'Class',
|
|
qualifiedName: 'Target',
|
|
filePath: 'source.ts',
|
|
});
|
|
const module = mkScope({ id: 'module', parent: null });
|
|
const inner: Scope = {
|
|
...mkScope({ id: 'inner', parent: module.id, kind: 'Function' }),
|
|
lexicalNames: new Set(['Alias']),
|
|
};
|
|
const imported: ReferenceSite = {
|
|
name: 'Alias',
|
|
kind: 'type-reference',
|
|
inScope: module.id,
|
|
atRange: range(3, 0, 3, 5),
|
|
};
|
|
const shadowed: ReferenceSite = { ...imported, inScope: inner.id, atRange: range(5, 0, 5, 5) };
|
|
const importedBindings = new Map<ScopeId, ReadonlyMap<string, readonly BindingRef[]>>();
|
|
const indexes = {
|
|
...makeIndexes([module, inner], [definition], [imported, shadowed]),
|
|
bindings: importedBindings,
|
|
};
|
|
importedBindings.set(
|
|
module.id,
|
|
new Map([
|
|
[
|
|
'Alias',
|
|
[
|
|
{
|
|
def: definition,
|
|
origin: 'import',
|
|
via: {
|
|
kind: 'alias',
|
|
localName: 'Alias',
|
|
targetExportedName: 'Target',
|
|
targetFile: definition.filePath,
|
|
targetDefId: definition.nodeId,
|
|
},
|
|
},
|
|
],
|
|
],
|
|
]),
|
|
);
|
|
const bindings = new Map<ReferenceSite, SourceBindingResolution>();
|
|
const result = resolveReferenceSites({
|
|
scopes: indexes,
|
|
ownedMembersByOwner: () => [],
|
|
recordSourceBinding: (site, binding) => bindings.set(site, binding),
|
|
});
|
|
expect(
|
|
result.referenceIndex.bySourceScope.get(module.id)?.map((reference) => reference.toDef),
|
|
).toEqual([definition.nodeId]);
|
|
expect(result.referenceIndex.bySourceScope.get(inner.id)).toBeUndefined();
|
|
expect(bindings.get(imported)).toMatchObject({
|
|
status: 'resolved',
|
|
targetDefIds: [definition.nodeId],
|
|
bindingKind: 'import-alias',
|
|
});
|
|
expect(bindings.get(shadowed)).toMatchObject({ status: 'blocked', targetDefIds: [] });
|
|
});
|
|
|
|
it('retains every finalized import candidate instead of promoting the ranked first match', () => {
|
|
const definitions = ['left', 'right'].map((name) =>
|
|
mkDef({
|
|
nodeId: `${name}.Target`,
|
|
type: 'Class',
|
|
qualifiedName: 'Target',
|
|
filePath: `${name}.ts`,
|
|
}),
|
|
);
|
|
const module = mkScope({ id: 'module', parent: null });
|
|
const site: ReferenceSite = {
|
|
name: 'Target',
|
|
kind: 'type-reference',
|
|
inScope: module.id,
|
|
atRange: range(3, 0, 3, 6),
|
|
};
|
|
const importedBindings = new Map<ScopeId, ReadonlyMap<string, readonly BindingRef[]>>();
|
|
const indexes = {
|
|
...makeIndexes([module], definitions, [site]),
|
|
bindings: importedBindings,
|
|
};
|
|
importedBindings.set(
|
|
module.id,
|
|
new Map([['Target', definitions.map((def) => ({ def, origin: 'import' as const }))]]),
|
|
);
|
|
const bindings = new Map<ReferenceSite, SourceBindingResolution>();
|
|
resolveReferenceSites({
|
|
scopes: indexes,
|
|
ownedMembersByOwner: () => [],
|
|
recordSourceBinding: (reference, binding) => bindings.set(reference, binding),
|
|
});
|
|
expect(bindings.get(site)).toMatchObject({
|
|
status: 'ambiguous',
|
|
candidatesComplete: true,
|
|
targetDefIds: definitions.map((def) => def.nodeId),
|
|
});
|
|
});
|
|
|
|
it('does not promote a global type-name fallback into an exact receiver binding', () => {
|
|
const owner = mkDef({
|
|
nodeId: 'external-owner',
|
|
type: 'Class',
|
|
qualifiedName: 'External',
|
|
filePath: 'unimported.ts',
|
|
});
|
|
const member = mkDef({
|
|
nodeId: 'external-owner.run',
|
|
type: 'Method',
|
|
qualifiedName: 'External.run',
|
|
ownerId: owner.nodeId,
|
|
filePath: owner.filePath,
|
|
});
|
|
const scope = mkScope({
|
|
id: 'scope:call',
|
|
parent: null,
|
|
typeBindings: { service: typeRef('External', 'scope:call') },
|
|
});
|
|
const site: ReferenceSite = {
|
|
name: 'run',
|
|
kind: 'call',
|
|
callForm: 'member',
|
|
explicitReceiver: { name: 'service' },
|
|
inScope: scope.id,
|
|
atRange: range(5, 1, 5, 14),
|
|
arity: 0,
|
|
};
|
|
const indexes = makeIndexes([scope], [owner, member], [site]);
|
|
const parsed = {
|
|
filePath: 'x.ts',
|
|
moduleScope: scope.id,
|
|
scopes: [scope],
|
|
localDefs: [],
|
|
parsedImports: [],
|
|
referenceSites: [site],
|
|
};
|
|
const model = createSemanticModel();
|
|
model.methods.register(owner.nodeId, 'run', member);
|
|
const graph = createKnowledgeGraph();
|
|
graph.addNode({ id: 'file', label: 'File', properties: { name: 'x.ts', filePath: 'x.ts' } });
|
|
graph.addNode({
|
|
id: member.nodeId,
|
|
label: 'Method',
|
|
properties: { name: 'run', qualifiedName: member.qualifiedName, filePath: member.filePath },
|
|
});
|
|
const bindings = new Map<ReferenceSite, SourceBindingResolution>();
|
|
emitReceiverBoundCalls(
|
|
graph,
|
|
indexes,
|
|
[parsed],
|
|
buildGraphNodeLookup(graph),
|
|
new Set(),
|
|
{ isSuperReceiver: () => false, arityCompatibility: () => 'compatible' },
|
|
buildWorkspaceResolutionIndex([parsed]),
|
|
model,
|
|
{ recordSourceBinding: (reference, binding) => bindings.set(reference, binding) },
|
|
);
|
|
expect(
|
|
graph.relationships.filter((relationship) => relationship.type === 'CALLS'),
|
|
).toHaveLength(1);
|
|
expect(bindings.get(site)).toMatchObject({
|
|
status: 'unresolved',
|
|
targetDefIds: [member.nodeId],
|
|
});
|
|
let guesses = 0;
|
|
expect(
|
|
resolveCompoundReceiverClass(
|
|
'service',
|
|
scope.id,
|
|
indexes,
|
|
buildWorkspaceResolutionIndex([parsed]),
|
|
{
|
|
onNameFallback: () => {
|
|
guesses++;
|
|
},
|
|
},
|
|
),
|
|
).toBe(owner);
|
|
expect(guesses).toBe(1);
|
|
});
|
|
|
|
it('observes blocked lexical names and explicit same-spelling import aliases', () => {
|
|
const definition = mkDef({ nodeId: 'def:Payload', type: 'Class', qualifiedName: 'Payload' });
|
|
const module = mkScope({
|
|
id: 'module',
|
|
parent: null,
|
|
bindings: {
|
|
Payload: [
|
|
{
|
|
def: definition,
|
|
origin: 'import',
|
|
via: {
|
|
kind: 'alias',
|
|
localName: 'Payload',
|
|
targetExportedName: 'Payload',
|
|
targetFile: 'types.ts',
|
|
targetDefId: definition.nodeId,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
const inner: Scope = {
|
|
...mkScope({ id: 'inner', parent: module.id, kind: 'Function' }),
|
|
lexicalNames: new Set(['blocked']),
|
|
};
|
|
const alias: ReferenceSite = {
|
|
name: 'Payload',
|
|
kind: 'type-reference',
|
|
inScope: inner.id,
|
|
atRange: range(5, 1, 5, 8),
|
|
};
|
|
const blocked: ReferenceSite = {
|
|
name: 'blocked',
|
|
kind: 'read',
|
|
inScope: inner.id,
|
|
atRange: range(6, 1, 6, 8),
|
|
};
|
|
const bindings = new Map<ReferenceSite, SourceBindingResolution>();
|
|
resolveReferenceSites({
|
|
scopes: makeIndexes([module, inner], [definition], [alias, blocked]),
|
|
ownedMembersByOwner: () => [],
|
|
recordSourceBinding: (site, binding) => bindings.set(site, binding),
|
|
});
|
|
expect(bindings.get(blocked)).toMatchObject({ status: 'blocked', basis: 'lexical-name-claim' });
|
|
expect(bindings.get(alias)).toMatchObject({
|
|
status: 'resolved',
|
|
targetDefIds: [definition.nodeId],
|
|
bindingKind: 'import-alias',
|
|
importAlias: {
|
|
localName: 'Payload',
|
|
importedName: 'Payload',
|
|
targetFile: 'types.ts',
|
|
scopeId: module.id,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('keeps an interface written binding separate from runtime dispatch targets', () => {
|
|
const api = mkDef({ nodeId: 'api', type: 'Interface', qualifiedName: 'Api' });
|
|
const implementation = mkDef({ nodeId: 'impl', type: 'Class', qualifiedName: 'Impl' });
|
|
const declared = mkDef({
|
|
nodeId: 'api.run',
|
|
type: 'Method',
|
|
qualifiedName: 'Api.run',
|
|
ownerId: api.nodeId,
|
|
});
|
|
const runtime = mkDef({
|
|
nodeId: 'impl.run',
|
|
type: 'Method',
|
|
qualifiedName: 'Impl.run',
|
|
ownerId: implementation.nodeId,
|
|
});
|
|
const scope = mkScope({
|
|
id: 'scope:call',
|
|
parent: null,
|
|
bindings: { Api: [{ def: api, origin: 'local' }] },
|
|
typeBindings: { service: typeRef('Api', 'scope:call') },
|
|
ownedDefs: [api, implementation, declared, runtime],
|
|
});
|
|
const site: ReferenceSite = {
|
|
name: 'run',
|
|
kind: 'call',
|
|
callForm: 'member',
|
|
explicitReceiver: { name: 'service' },
|
|
inScope: scope.id,
|
|
atRange: range(5, 1, 5, 14),
|
|
arity: 0,
|
|
};
|
|
const indexes = makeIndexes([scope], [api, implementation, declared, runtime], [site]);
|
|
const parsed = {
|
|
filePath: 'x.ts',
|
|
moduleScope: scope.id,
|
|
scopes: [scope],
|
|
localDefs: [api, implementation, declared, runtime],
|
|
parsedImports: [],
|
|
referenceSites: [site],
|
|
};
|
|
const model = createSemanticModel();
|
|
model.methods.register(api.nodeId, 'run', declared);
|
|
model.methods.register(implementation.nodeId, 'run', runtime);
|
|
const graph = createKnowledgeGraph();
|
|
graph.addNode({ id: 'file', label: 'File', properties: { name: 'x.ts', filePath: 'x.ts' } });
|
|
for (const definition of parsed.localDefs)
|
|
graph.addNode({
|
|
id: definition.nodeId,
|
|
label: definition.type,
|
|
properties: {
|
|
name: definition.qualifiedName!.split('.').at(-1)!,
|
|
qualifiedName: definition.qualifiedName,
|
|
filePath: definition.filePath,
|
|
},
|
|
});
|
|
graph.addRelationship({
|
|
id: 'implements',
|
|
sourceId: implementation.nodeId,
|
|
targetId: api.nodeId,
|
|
type: 'IMPLEMENTS',
|
|
confidence: 1,
|
|
reason: 'test',
|
|
});
|
|
const bindings = new Map<ReferenceSite, SourceBindingResolution>();
|
|
emitReceiverBoundCalls(
|
|
graph,
|
|
indexes,
|
|
[parsed],
|
|
buildGraphNodeLookup(graph),
|
|
new Set(),
|
|
{ isSuperReceiver: () => false, arityCompatibility: () => 'compatible' },
|
|
buildWorkspaceResolutionIndex([parsed]),
|
|
model,
|
|
{ recordSourceBinding: (reference, binding) => bindings.set(reference, binding) },
|
|
);
|
|
expect(
|
|
graph.relationships
|
|
.filter((relationship) => relationship.type === 'CALLS')
|
|
.map((relationship) => relationship.targetId)
|
|
.sort(),
|
|
).toEqual([declared.nodeId, runtime.nodeId]);
|
|
expect(bindings.get(site)).toMatchObject({
|
|
status: 'resolved',
|
|
targetDefIds: [declared.nodeId],
|
|
});
|
|
});
|
|
|
|
it('observes all candidates without certifying the registry tie break as a binding', () => {
|
|
const first = mkDef({ nodeId: 'def:first', type: 'Function', qualifiedName: 'save' });
|
|
const second = mkDef({ nodeId: 'def:second', type: 'Function', qualifiedName: 'save' });
|
|
const scope = mkScope({
|
|
id: 'scope:call',
|
|
parent: null,
|
|
bindings: {
|
|
save: [
|
|
{ def: first, origin: 'local' },
|
|
{ def: second, origin: 'local' },
|
|
],
|
|
},
|
|
});
|
|
const site: ReferenceSite = {
|
|
name: 'save',
|
|
kind: 'call',
|
|
inScope: scope.id,
|
|
atRange: range(5, 2, 5, 8),
|
|
};
|
|
const bindings = new Map<ReferenceSite, { status: string; targetDefIds: readonly string[] }>();
|
|
const output = resolveReferenceSites({
|
|
scopes: makeIndexes([scope], [first, second], [site]),
|
|
ownedMembersByOwner: () => [],
|
|
recordSourceBinding: (reference, binding) => bindings.set(reference, binding),
|
|
});
|
|
expect(output.stats.referencesEmitted).toBe(1);
|
|
expect(bindings.get(site)).toMatchObject({
|
|
status: 'ambiguous',
|
|
targetDefIds: [first.nodeId, second.nodeId],
|
|
});
|
|
});
|
|
|
|
it('uses ownedMembersByOwner to resolve a hook-provided receiver member', () => {
|
|
const userClass = mkDef({ nodeId: 'def:User', type: 'Class', qualifiedName: 'User' });
|
|
const saveMethod = mkDef({
|
|
nodeId: 'def:User.save',
|
|
type: 'Method',
|
|
qualifiedName: 'User.save',
|
|
ownerId: 'def:User',
|
|
});
|
|
const scope = mkScope({
|
|
id: 'scope:call',
|
|
parent: null,
|
|
typeBindings: { user: typeRef('User', 'scope:call') },
|
|
});
|
|
const referenceSite: ReferenceSite = {
|
|
name: 'save',
|
|
atRange: range(5, 2, 5, 6),
|
|
inScope: 'scope:call',
|
|
kind: 'call',
|
|
explicitReceiver: { name: 'user' },
|
|
arity: 0,
|
|
};
|
|
const indexes = makeIndexes([scope], [userClass], [referenceSite]);
|
|
|
|
const result = resolveReferenceSites({
|
|
scopes: indexes,
|
|
ownedMembersByOwner: (ownerDefId, memberName) =>
|
|
ownerDefId === 'def:User' && memberName === 'save' ? [saveMethod] : [],
|
|
});
|
|
|
|
expect(result.stats).toEqual({ sitesProcessed: 1, referencesEmitted: 1, unresolved: 0 });
|
|
expect(result.referenceIndex.bySourceScope.get('scope:call')).toHaveLength(1);
|
|
expect(result.referenceIndex.bySourceScope.get('scope:call')?.[0]?.toDef).toBe('def:User.save');
|
|
});
|
|
|
|
it('threads providers.arityCompatibility through to filter hook-provided overloads', () => {
|
|
const userClass = mkDef({ nodeId: 'def:User', type: 'Class', qualifiedName: 'User' });
|
|
const saveOne = mkDef({
|
|
nodeId: 'def:User.save#1',
|
|
type: 'Method',
|
|
qualifiedName: 'User.save',
|
|
ownerId: 'def:User',
|
|
parameterCount: 1,
|
|
});
|
|
const saveTwo = mkDef({
|
|
nodeId: 'def:User.save#2',
|
|
type: 'Method',
|
|
qualifiedName: 'User.save',
|
|
ownerId: 'def:User',
|
|
parameterCount: 2,
|
|
});
|
|
const scope = mkScope({
|
|
id: 'scope:call',
|
|
parent: null,
|
|
typeBindings: { user: typeRef('User', 'scope:call') },
|
|
});
|
|
const referenceSite: ReferenceSite = {
|
|
name: 'save',
|
|
atRange: range(5, 2, 5, 6),
|
|
inScope: 'scope:call',
|
|
kind: 'call',
|
|
explicitReceiver: { name: 'user' },
|
|
arity: 1,
|
|
};
|
|
const indexes = makeIndexes([scope], [userClass], [referenceSite]);
|
|
|
|
const result = resolveReferenceSites({
|
|
scopes: indexes,
|
|
ownedMembersByOwner: (ownerDefId, memberName) =>
|
|
ownerDefId === 'def:User' && memberName === 'save' ? [saveOne, saveTwo] : [],
|
|
providers: {
|
|
arityCompatibility: (callsite, def) =>
|
|
def.parameterCount === callsite.arity ? 'compatible' : 'incompatible',
|
|
},
|
|
});
|
|
|
|
expect(result.stats).toEqual({ sitesProcessed: 1, referencesEmitted: 1, unresolved: 0 });
|
|
expect(result.referenceIndex.bySourceScope.get('scope:call')).toHaveLength(1);
|
|
expect(result.referenceIndex.bySourceScope.get('scope:call')?.[0]?.toDef).toBe(
|
|
'def:User.save#1',
|
|
);
|
|
});
|
|
});
|