mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-09 22:33:39 +00:00
fix(scope-resolution): mark construction-site CALLS edges in reason (opt-in), enable for Zig
PR #1432 human review, item 2: a Zig struct literal `T{ .f = x }` (no parens) is modelled as a CALLS edge to the type — the Rust `T { .. }` / Go `T{}` shape — and nothing on the edge told it apart from an invocation (`get_next_spawn → SpawnRequest` from seven `return SpawnRequest{ … }`). `ScopeResolver.markConstructionSites` (default off): when set, the edge emitted for a `callForm === 'constructor'` site gets ` (constructor)` appended to its reason, in both emit paths — `local-call (constructor)` / `import-resolved (constructor)` in the free-call fallback and `scope-resolution: call (constructor)` in the reference bridge. The Zig resolver opts in. `Reference` gains an optional `callForm`, copied from the site by `buildReference`, so the bridge can see the form. Why `reason` and not a property or edge type: relationships carry no arbitrary properties, a new column changes the relation DDL and moves SCHEMA_FINGERPRINT, and `reason` is the channel the IMPLEMENTS `-pointer` receiver form already uses. Why opt-in: the unsuffixed strings are a pinned contract asserted verbatim by the other language suites (php/cpp constructor calls expect exactly `import-resolved`); every non-Zig edge stays byte-identical. Tests: `references-to-edges-call-form.test.ts` pins both vocabularies and the default-off behaviour; `zig.test.ts` asserts `Reflect.string → Accessor` / `Reflect.url → Accessor` carry `local-call (constructor)` next to a plain invocation, and that every marked edge targets a Struct.
This commit is contained in:
parent
c740d716be
commit
97571d23f5
9 changed files with 368 additions and 4 deletions
|
|
@ -24,6 +24,9 @@
|
|||
|
||||
import type { NodeLabel } from '../graph/types.js';
|
||||
import type { SymbolDefinition } from './symbol-definition.js';
|
||||
// Type-only, so the `reference-site.ts` → `types.ts` import cycle is erased
|
||||
// at compile time.
|
||||
import type { CallForm } from './reference-site.js';
|
||||
|
||||
// ─── §2.1 Type aliases ──────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -752,6 +755,15 @@ export interface Reference {
|
|||
| 'import-use'
|
||||
| 'value-ref'
|
||||
| 'macro';
|
||||
/**
|
||||
* Call form of the site this reference was resolved from, copied verbatim
|
||||
* from `ReferenceSite.callForm`; set only when `kind === 'call'`. The
|
||||
* emit phase reads it to tell a construction site (`T{…}`, `new T()`,
|
||||
* `T { .. }` — form `'constructor'`) apart from an invocation, which in the
|
||||
* graph are both `CALLS` edges. Optional and additive: a `Reference` built
|
||||
* without it is emitted exactly as before.
|
||||
*/
|
||||
readonly callForm?: CallForm;
|
||||
readonly confidence: number;
|
||||
readonly evidence: readonly ResolutionEvidence[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ export const zigScopeResolver: ScopeResolver = {
|
|||
language: SupportedLanguages.Zig,
|
||||
languageProvider: zigProvider,
|
||||
importEdgeReason: 'zig-scope: import',
|
||||
// A struct literal `T{ .f = x }` is a CALLS edge to the Struct node (the
|
||||
// Rust `T { .. }` / Go `T{}` shape). Zig has no Constructor nodes, so
|
||||
// nothing but this marker tells that edge from an invocation on the edge
|
||||
// itself — `main → SpawnRequest` looked like a call to a function
|
||||
// (PR #1432 review). Emits `local-call (constructor)` and friends.
|
||||
markConstructionSites: true,
|
||||
|
||||
loadResolutionConfig: (repoPath: string) => loadZigBuildConfig(repoPath),
|
||||
|
||||
|
|
|
|||
|
|
@ -322,6 +322,10 @@ function buildReference(site: ReferenceSite, top: Resolution): Reference {
|
|||
toDef: top.def.nodeId,
|
||||
atRange: site.atRange,
|
||||
kind: site.kind,
|
||||
// The call form survives resolution so the graph bridge can mark
|
||||
// construction sites (`callForm: 'constructor'`) on the CALLS edge it
|
||||
// emits — a `Reference` otherwise keeps only the resolved def.
|
||||
...(site.kind === 'call' && site.callForm !== undefined ? { callForm: site.callForm } : {}),
|
||||
confidence: top.confidence,
|
||||
evidence: top.evidence,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -878,6 +878,30 @@ export interface ScopeResolver {
|
|||
*/
|
||||
readonly constructorCallTargetsClass?: boolean;
|
||||
|
||||
/**
|
||||
* When true, the CALLS edge emitted for a constructor-form site
|
||||
* (`callForm === 'constructor'`) carries ` (constructor)` appended to its
|
||||
* `reason` — `local-call (constructor)`, `import-resolved (constructor)`,
|
||||
* `scope-resolution: call (constructor)` — so a consumer can tell
|
||||
* "constructs an instance of" apart from "invokes" on the edge alone.
|
||||
*
|
||||
* Opt-in because the unsuffixed strings are a pinned contract: the legacy
|
||||
* DAG vocabulary (`'import-resolved' | 'local-call' | …`, see the
|
||||
* same-graph guarantee above) is asserted verbatim by consumers and by the
|
||||
* per-language resolver suites, constructor sites included. A language
|
||||
* that links a construction site to the TYPE node itself — a struct
|
||||
* literal `T{…}` in Zig, where nothing but the marker distinguishes the
|
||||
* edge from an invocation in the schema (PR #1432 review) — opts in; the
|
||||
* default leaves every existing edge byte-identical.
|
||||
*
|
||||
* The marker rides in `reason` because relationships carry no arbitrary
|
||||
* properties (adding one moves SCHEMA_FINGERPRINT — the IMPLEMENTS
|
||||
* `-pointer` precedent in `pipeline/run.ts`). Applies to the free-call
|
||||
* fallback and the reference bridge; receiver-qualified construction sites
|
||||
* are not tagged `constructor` by any provider today.
|
||||
*/
|
||||
readonly markConstructionSites?: boolean;
|
||||
|
||||
/**
|
||||
* How this language spells a construction expression, so the compound
|
||||
* receiver resolver can type an INLINE constructor receiver — the
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
* chain looking for an enclosing Function/Method/Class.
|
||||
* 2. Resolve `toDef` → target graph-node id via `nodeLookup`.
|
||||
* 3. Emit the edge (`CALLS` / `READS` / `WRITES` / `EXTENDS` / `USES`)
|
||||
* with the standard reason format.
|
||||
* with the standard reason format (`referenceEdgeReason`).
|
||||
*
|
||||
* Skips (without throwing) when either side fails to map — either side
|
||||
* may legitimately not exist as a graph node (e.g. a resolved target
|
||||
|
|
@ -36,6 +36,36 @@ import { isValueDefinitionLabel } from '../../utils/ast-helpers.js';
|
|||
*/
|
||||
type ReferenceSiteSkipSet = ReadonlySet<string>;
|
||||
|
||||
export interface EmitReferencesOptions {
|
||||
/** When true, a constructor-form call site's edge gets ` (constructor)`
|
||||
* appended to its reason. See `ScopeResolver.markConstructionSites`. */
|
||||
readonly markConstructionSites?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* `reason` of the edge a resolved reference emits: `scope-resolution: <kind>`,
|
||||
* plus ` (constructor)` for a construction site when the provider opted in —
|
||||
* a struct literal (`T{…}` in Zig, `T { .. }` in Rust, `T{}` in Go) or a
|
||||
* `new T()` resolves to the type (or its constructor) as a CALLS edge exactly
|
||||
* like an invocation does, and nothing else on the edge tells the two apart
|
||||
* (PR #1432 review).
|
||||
*
|
||||
* The marker rides in `reason` because relationships carry no arbitrary
|
||||
* properties — adding one would change the relation DDL and move
|
||||
* SCHEMA_FINGERPRINT (see the IMPLEMENTS `-pointer` precedent in
|
||||
* `pipeline/run.ts`). The plain `scope-resolution: call` prefix is kept, so a
|
||||
* consumer matching the prefix still sees every call; one matching the exact
|
||||
* string sees invocations only.
|
||||
*/
|
||||
export function referenceEdgeReason(
|
||||
ref: Pick<Reference, 'kind' | 'callForm'>,
|
||||
markConstructionSites: boolean | undefined,
|
||||
): string {
|
||||
return markConstructionSites === true && ref.kind === 'call' && ref.callForm === 'constructor'
|
||||
? 'scope-resolution: call (constructor)'
|
||||
: `scope-resolution: ${ref.kind}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Value labels whose defs MAY be function-local. A reference to one of these is
|
||||
* dropped only when the def is positively identified as living inside a function
|
||||
|
|
@ -76,6 +106,7 @@ export function emitReferencesViaLookup(
|
|||
* Optional so callers that never capture bare identifiers are unchanged.
|
||||
*/
|
||||
functionLocalValueDefIds?: ReadonlySet<string>,
|
||||
options?: EmitReferencesOptions,
|
||||
): { emitted: number; skipped: number } {
|
||||
let emitted = 0;
|
||||
let skipped = 0;
|
||||
|
|
@ -153,7 +184,7 @@ export function emitReferencesViaLookup(
|
|||
targetId: targetGraphId,
|
||||
type: edgeType,
|
||||
confidence: ref.confidence,
|
||||
reason: `scope-resolution: ${ref.kind}`,
|
||||
reason: referenceEdgeReason(ref, options?.markConstructionSites),
|
||||
});
|
||||
emitted++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import type {
|
|||
ParameterTypeClass,
|
||||
ParsedFile,
|
||||
Reference,
|
||||
ReferenceSite,
|
||||
ScopeId,
|
||||
SymbolDefinition,
|
||||
} from 'gitnexus-shared';
|
||||
|
|
@ -66,6 +67,9 @@ export function emitFreeCallFallback(
|
|||
/** When true, `Type(...)` constructor calls link to the Class def
|
||||
* itself rather than its explicit Constructor. Swift opts in. */
|
||||
readonly constructorCallTargetsClass?: boolean;
|
||||
/** When true, a constructor-form site's edge gets ` (constructor)`
|
||||
* appended to its reason. See `ScopeResolver.markConstructionSites`. */
|
||||
readonly markConstructionSites?: boolean;
|
||||
readonly isFileLocalDef?: (def: SymbolDefinition) => boolean;
|
||||
readonly isCallableVisibleFromCaller?: (ctx: {
|
||||
readonly callerParsed: ParsedFile;
|
||||
|
|
@ -621,8 +625,13 @@ export function emitFreeCallFallback(
|
|||
type: 'CALLS',
|
||||
confidence: 0.85,
|
||||
// Match legacy DAG's reason convention so consumers that
|
||||
// assert `reason === 'import-resolved'` keep working.
|
||||
reason: fnDef.filePath !== parsed.filePath ? 'import-resolved' : 'local-call',
|
||||
// assert `reason === 'import-resolved'` keep working. The
|
||||
// construction-site marker is opt-in for the same reason.
|
||||
reason: constructionSiteReason(
|
||||
fnDef.filePath !== parsed.filePath ? 'import-resolved' : 'local-call',
|
||||
site,
|
||||
options.markConstructionSites,
|
||||
),
|
||||
});
|
||||
emitted++;
|
||||
}
|
||||
|
|
@ -630,6 +639,19 @@ export function emitFreeCallFallback(
|
|||
return emitted;
|
||||
}
|
||||
|
||||
/** `reason` of a free-call edge: the legacy string, plus ` (constructor)` for
|
||||
* a construction site when the provider opted in
|
||||
* (`ScopeResolver.markConstructionSites`). */
|
||||
export function constructionSiteReason(
|
||||
base: string,
|
||||
site: Pick<ReferenceSite, 'callForm'>,
|
||||
markConstructionSites: boolean | undefined,
|
||||
): string {
|
||||
return markConstructionSites === true && site.callForm === 'constructor'
|
||||
? `${base} (constructor)`
|
||||
: base;
|
||||
}
|
||||
|
||||
function siteKey(
|
||||
filePath: string,
|
||||
site: { readonly atRange: { readonly startLine: number; readonly startCol: number } },
|
||||
|
|
|
|||
|
|
@ -1070,6 +1070,7 @@ export function runScopeResolution(
|
|||
{
|
||||
allowGlobalFallback: provider.allowGlobalFreeCallFallback === true,
|
||||
constructorCallTargetsClass: provider.constructorCallTargetsClass === true,
|
||||
markConstructionSites: provider.markConstructionSites === true,
|
||||
isFileLocalDef: provider.isFileLocalDef,
|
||||
isBuiltInName: provider.languageProvider.isBuiltInName,
|
||||
freeCallsRequireInstanceOwnership: provider.freeCallsRequireInstanceOwnership === true,
|
||||
|
|
@ -1100,6 +1101,7 @@ export function runScopeResolution(
|
|||
// both correctly emit. See the build site above for why the earlier
|
||||
// allowlist could not be made safe this way.
|
||||
functionLocalValueDefIds,
|
||||
{ markConstructionSites: provider.markConstructionSites === true },
|
||||
);
|
||||
// Last-resort property resolution by workspace-unique name (A1/A5). Runs
|
||||
// after every precise pass and only sees what they left behind, so a
|
||||
|
|
|
|||
|
|
@ -666,6 +666,32 @@ describe.skipIf(!zigAvailable)('Zig function-local and anonymous containers (F8)
|
|||
);
|
||||
});
|
||||
|
||||
it('marks a struct-literal construction site (`return Accessor{…}`) on its CALLS edge, unlike an invocation', () => {
|
||||
// A struct literal `T{ .f = x }` (no parens) is deliberately modelled as a
|
||||
// CALLS edge to the type — the same shape Rust `T { .. }` and Go `T{}`
|
||||
// produce. The PR #1432 review found it indistinguishable from a real
|
||||
// call: the marker in `reason` is what lets a consumer tell "constructs an
|
||||
// instance of" apart from "invokes". `Reflect.string` / `Reflect.url`
|
||||
// each `return Accessor{ .get = R.get, .set = R.set }` (reflect.zig).
|
||||
// Same-file free-call fallback vocabulary, suffixed because the Zig
|
||||
// resolver opts into `markConstructionSites`.
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const reasonsOf = (source: string, target: string): string[] =>
|
||||
calls.filter((e) => e.source === source && e.target === target).map((e) => e.rel.reason);
|
||||
expect(reasonsOf('string', 'Accessor')).toEqual(['local-call (constructor)']);
|
||||
expect(reasonsOf('url', 'Accessor')).toEqual(['local-call (constructor)']);
|
||||
// The invocation next door keeps its plain reason: `util.helper()` in
|
||||
// main.zig is a call, not a construction site.
|
||||
expect(reasonsOf('main', 'helper')).toHaveLength(1);
|
||||
expect(reasonsOf('main', 'helper')[0]).not.toContain('(constructor)');
|
||||
// No construction site is emitted as anything but CALLS, and every other
|
||||
// CALLS edge is an invocation.
|
||||
const constructionSites = calls.filter((e) => e.rel.reason.endsWith('(constructor)'));
|
||||
expect(constructionSites.map((e) => e.targetLabel)).toEqual(
|
||||
constructionSites.map(() => 'Struct'),
|
||||
);
|
||||
});
|
||||
|
||||
it('gives anonymous containers a host + ordinal identity, so no Method is ownerless and same-named fns never collide', () => {
|
||||
expect(idsIn('Struct', 'Sorter.zig')).toEqual([
|
||||
'Struct:src/Sorter.zig:Sorter',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
/**
|
||||
* The CALLS edge a construction site emits must be distinguishable from the
|
||||
* one an invocation emits (PR #1432 review, item 2).
|
||||
*
|
||||
* A struct literal (`SpawnRequest{ .a = x }` in Zig, `Foo { a }` in Rust,
|
||||
* `Foo{}` in Go) and a `new Foo()` are tagged `@reference.call.constructor`
|
||||
* by their language queries and resolve to the type (or its constructor) as
|
||||
* an ordinary `kind: 'call'` reference. Both land in the graph as `CALLS`,
|
||||
* and relationships carry no free-form properties (adding one moves
|
||||
* SCHEMA_FINGERPRINT), so the call form rides in `reason` — the same channel
|
||||
* the IMPLEMENTS `-pointer` receiver form uses.
|
||||
*
|
||||
* Why the assertions are exact strings: a consumer that filters by the
|
||||
* exact `scope-resolution: call` is promised invocations only, and one that
|
||||
* matches the prefix is promised every call. Loosening either side would
|
||||
* silently break one of the two contracts.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
buildDefIndex,
|
||||
buildMethodDispatchIndex,
|
||||
buildModuleScopeIndex,
|
||||
buildQualifiedNameIndex,
|
||||
buildScopeTree,
|
||||
type NodeLabel,
|
||||
type Range,
|
||||
type Reference,
|
||||
type Scope,
|
||||
type ScopeId,
|
||||
type SymbolDefinition,
|
||||
} from 'gitnexus-shared';
|
||||
import { createKnowledgeGraph } from '../../../src/core/graph/graph.js';
|
||||
import type { ScopeResolutionIndexes } from '../../../src/core/ingestion/model/scope-resolution-indexes.js';
|
||||
import { buildGraphNodeLookup } from '../../../src/core/ingestion/scope-resolution/graph-bridge/node-lookup.js';
|
||||
import {
|
||||
emitReferencesViaLookup,
|
||||
referenceEdgeReason,
|
||||
} from '../../../src/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js';
|
||||
import { constructionSiteReason } from '../../../src/core/ingestion/scope-resolution/passes/free-call-fallback.js';
|
||||
|
||||
const FILE = 'x.zig';
|
||||
|
||||
const range = (sl: number, sc: number): Range => ({
|
||||
startLine: sl,
|
||||
startCol: sc,
|
||||
endLine: sl,
|
||||
endCol: sc + 4,
|
||||
});
|
||||
|
||||
const def = (nodeId: string, type: SymbolDefinition['type'], qname: string): SymbolDefinition => ({
|
||||
nodeId,
|
||||
filePath: FILE,
|
||||
type,
|
||||
qualifiedName: qname,
|
||||
});
|
||||
|
||||
function moduleScope(ownedDefs: readonly SymbolDefinition[]): Scope {
|
||||
return {
|
||||
id: 'scope:m',
|
||||
parent: null,
|
||||
kind: 'Module',
|
||||
range: { startLine: 1, startCol: 0, endLine: 100, endCol: 0 },
|
||||
filePath: FILE,
|
||||
bindings: new Map(),
|
||||
ownedDefs,
|
||||
imports: [],
|
||||
typeBindings: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
function makeIndexes(scope: Scope, allDefs: readonly SymbolDefinition[]): ScopeResolutionIndexes {
|
||||
return {
|
||||
scopeTree: buildScopeTree([scope]),
|
||||
defs: buildDefIndex([...allDefs]),
|
||||
qualifiedNames: buildQualifiedNameIndex([...allDefs]),
|
||||
moduleScopes: buildModuleScopeIndex([{ filePath: FILE, moduleScopeId: scope.id }]),
|
||||
methodDispatch: buildMethodDispatchIndex({
|
||||
owners: [],
|
||||
computeMro: () => [],
|
||||
implementsOf: () => [],
|
||||
}),
|
||||
imports: new Map(),
|
||||
bindings: new Map(),
|
||||
bindingAugmentations: new Map(),
|
||||
workspaceFqnBindings: new Map(),
|
||||
workspaceTypeBindings: new Map(),
|
||||
namespaceFqnBindings: new Map(),
|
||||
namespaceTypeBindings: new Map(),
|
||||
accessibleNamespacesByScope: new Map(),
|
||||
referenceSites: [],
|
||||
sccs: [],
|
||||
stats: {
|
||||
totalFiles: 0,
|
||||
totalEdges: 0,
|
||||
linkedEdges: 0,
|
||||
unresolvedEdges: 0,
|
||||
sccCount: 0,
|
||||
largestSccSize: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('referenceEdgeReason', () => {
|
||||
it('marks only the constructor call form; every other kind keeps the plain reason', () => {
|
||||
expect(referenceEdgeReason({ kind: 'call', callForm: 'constructor' }, true)).toBe(
|
||||
'scope-resolution: call (constructor)',
|
||||
);
|
||||
expect(referenceEdgeReason({ kind: 'call', callForm: 'free' }, true)).toBe(
|
||||
'scope-resolution: call',
|
||||
);
|
||||
expect(referenceEdgeReason({ kind: 'call', callForm: 'member' }, true)).toBe(
|
||||
'scope-resolution: call',
|
||||
);
|
||||
// A `Reference` minted before the field existed (or by a path that does
|
||||
// not set it) is emitted exactly as before.
|
||||
expect(referenceEdgeReason({ kind: 'call' }, true)).toBe('scope-resolution: call');
|
||||
expect(referenceEdgeReason({ kind: 'read' }, true)).toBe('scope-resolution: read');
|
||||
expect(referenceEdgeReason({ kind: 'type-reference' }, true)).toBe(
|
||||
'scope-resolution: type-reference',
|
||||
);
|
||||
});
|
||||
|
||||
it('is opt-in: a provider that did not set `markConstructionSites` gets the pinned string', () => {
|
||||
// The unsuffixed reasons are a contract asserted verbatim by the other
|
||||
// language suites; the marker must not leak into them by default.
|
||||
expect(referenceEdgeReason({ kind: 'call', callForm: 'constructor' }, undefined)).toBe(
|
||||
'scope-resolution: call',
|
||||
);
|
||||
expect(referenceEdgeReason({ kind: 'call', callForm: 'constructor' }, false)).toBe(
|
||||
'scope-resolution: call',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the plain `scope-resolution: call` as a prefix of the marked form', () => {
|
||||
// The compatibility promise: prefix matchers keep seeing construction
|
||||
// sites as calls.
|
||||
expect(
|
||||
referenceEdgeReason({ kind: 'call', callForm: 'constructor' }, true).startsWith(
|
||||
'scope-resolution: call',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('constructionSiteReason (free-call fallback vocabulary)', () => {
|
||||
it('suffixes the legacy `local-call` / `import-resolved` only for an opted-in constructor site', () => {
|
||||
expect(constructionSiteReason('local-call', { callForm: 'constructor' }, true)).toBe(
|
||||
'local-call (constructor)',
|
||||
);
|
||||
expect(constructionSiteReason('import-resolved', { callForm: 'constructor' }, true)).toBe(
|
||||
'import-resolved (constructor)',
|
||||
);
|
||||
expect(constructionSiteReason('local-call', { callForm: 'free' }, true)).toBe('local-call');
|
||||
expect(constructionSiteReason('local-call', { callForm: 'constructor' }, undefined)).toBe(
|
||||
'local-call',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('emitReferencesViaLookup — construction site vs invocation', () => {
|
||||
it('emits both as CALLS, and only the construction site carries the constructor marker', () => {
|
||||
// `fn get_next_spawn() SpawnRequest { helper(); return SpawnRequest{ .a = 1 }; }`
|
||||
const caller = def('def:get_next_spawn', 'Function', 'get_next_spawn');
|
||||
const helper = def('def:helper', 'Function', 'helper');
|
||||
const spawnRequest = def('def:SpawnRequest', 'Struct', 'SpawnRequest');
|
||||
const allDefs = [caller, helper, spawnRequest];
|
||||
const indexes = makeIndexes(moduleScope(allDefs), allDefs);
|
||||
|
||||
const graph = createKnowledgeGraph();
|
||||
graph.addNode({
|
||||
id: 'fn:get_next_spawn',
|
||||
label: 'Function' as NodeLabel,
|
||||
properties: { name: 'get_next_spawn', filePath: FILE, qualifiedName: 'get_next_spawn' },
|
||||
});
|
||||
graph.addNode({
|
||||
id: 'fn:helper',
|
||||
label: 'Function' as NodeLabel,
|
||||
properties: { name: 'helper', filePath: FILE, qualifiedName: 'helper' },
|
||||
});
|
||||
graph.addNode({
|
||||
id: 'struct:SpawnRequest',
|
||||
label: 'Struct' as NodeLabel,
|
||||
properties: { name: 'SpawnRequest', filePath: FILE, qualifiedName: 'SpawnRequest' },
|
||||
});
|
||||
|
||||
const invocation: Reference = {
|
||||
fromScope: 'scope:m',
|
||||
toDef: 'def:helper',
|
||||
atRange: range(2, 4),
|
||||
kind: 'call',
|
||||
callForm: 'free',
|
||||
confidence: 0.9,
|
||||
evidence: [],
|
||||
};
|
||||
const construction: Reference = {
|
||||
fromScope: 'scope:m',
|
||||
toDef: 'def:SpawnRequest',
|
||||
atRange: range(3, 11),
|
||||
kind: 'call',
|
||||
callForm: 'constructor',
|
||||
confidence: 0.9,
|
||||
evidence: [],
|
||||
};
|
||||
const referenceIndex = {
|
||||
bySourceScope: new Map<ScopeId, readonly Reference[]>([
|
||||
['scope:m', [invocation, construction]],
|
||||
]),
|
||||
};
|
||||
|
||||
const result = emitReferencesViaLookup(
|
||||
graph,
|
||||
indexes,
|
||||
referenceIndex,
|
||||
buildGraphNodeLookup(graph),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ markConstructionSites: true },
|
||||
);
|
||||
|
||||
expect(result).toEqual({ emitted: 2, skipped: 0 });
|
||||
const edges = graph.relationships.map((r) => ({
|
||||
type: r.type,
|
||||
targetId: r.targetId,
|
||||
reason: r.reason,
|
||||
}));
|
||||
expect(edges).toEqual([
|
||||
{ type: 'CALLS', targetId: 'fn:helper', reason: 'scope-resolution: call' },
|
||||
{
|
||||
type: 'CALLS',
|
||||
targetId: 'struct:SpawnRequest',
|
||||
reason: 'scope-resolution: call (constructor)',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue