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 { NodeLabel } from '../graph/types.js';
|
||||||
import type { SymbolDefinition } from './symbol-definition.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 ──────────────────────────────────────────────────────
|
// ─── §2.1 Type aliases ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -752,6 +755,15 @@ export interface Reference {
|
||||||
| 'import-use'
|
| 'import-use'
|
||||||
| 'value-ref'
|
| 'value-ref'
|
||||||
| 'macro';
|
| '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 confidence: number;
|
||||||
readonly evidence: readonly ResolutionEvidence[];
|
readonly evidence: readonly ResolutionEvidence[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ export const zigScopeResolver: ScopeResolver = {
|
||||||
language: SupportedLanguages.Zig,
|
language: SupportedLanguages.Zig,
|
||||||
languageProvider: zigProvider,
|
languageProvider: zigProvider,
|
||||||
importEdgeReason: 'zig-scope: import',
|
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),
|
loadResolutionConfig: (repoPath: string) => loadZigBuildConfig(repoPath),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,10 @@ function buildReference(site: ReferenceSite, top: Resolution): Reference {
|
||||||
toDef: top.def.nodeId,
|
toDef: top.def.nodeId,
|
||||||
atRange: site.atRange,
|
atRange: site.atRange,
|
||||||
kind: site.kind,
|
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,
|
confidence: top.confidence,
|
||||||
evidence: top.evidence,
|
evidence: top.evidence,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -878,6 +878,30 @@ export interface ScopeResolver {
|
||||||
*/
|
*/
|
||||||
readonly constructorCallTargetsClass?: boolean;
|
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
|
* How this language spells a construction expression, so the compound
|
||||||
* receiver resolver can type an INLINE constructor receiver — the
|
* receiver resolver can type an INLINE constructor receiver — the
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* chain looking for an enclosing Function/Method/Class.
|
* chain looking for an enclosing Function/Method/Class.
|
||||||
* 2. Resolve `toDef` → target graph-node id via `nodeLookup`.
|
* 2. Resolve `toDef` → target graph-node id via `nodeLookup`.
|
||||||
* 3. Emit the edge (`CALLS` / `READS` / `WRITES` / `EXTENDS` / `USES`)
|
* 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
|
* Skips (without throwing) when either side fails to map — either side
|
||||||
* may legitimately not exist as a graph node (e.g. a resolved target
|
* 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>;
|
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
|
* 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
|
* 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.
|
* Optional so callers that never capture bare identifiers are unchanged.
|
||||||
*/
|
*/
|
||||||
functionLocalValueDefIds?: ReadonlySet<string>,
|
functionLocalValueDefIds?: ReadonlySet<string>,
|
||||||
|
options?: EmitReferencesOptions,
|
||||||
): { emitted: number; skipped: number } {
|
): { emitted: number; skipped: number } {
|
||||||
let emitted = 0;
|
let emitted = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
|
|
@ -153,7 +184,7 @@ export function emitReferencesViaLookup(
|
||||||
targetId: targetGraphId,
|
targetId: targetGraphId,
|
||||||
type: edgeType,
|
type: edgeType,
|
||||||
confidence: ref.confidence,
|
confidence: ref.confidence,
|
||||||
reason: `scope-resolution: ${ref.kind}`,
|
reason: referenceEdgeReason(ref, options?.markConstructionSites),
|
||||||
});
|
});
|
||||||
emitted++;
|
emitted++;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import type {
|
||||||
ParameterTypeClass,
|
ParameterTypeClass,
|
||||||
ParsedFile,
|
ParsedFile,
|
||||||
Reference,
|
Reference,
|
||||||
|
ReferenceSite,
|
||||||
ScopeId,
|
ScopeId,
|
||||||
SymbolDefinition,
|
SymbolDefinition,
|
||||||
} from 'gitnexus-shared';
|
} from 'gitnexus-shared';
|
||||||
|
|
@ -66,6 +67,9 @@ export function emitFreeCallFallback(
|
||||||
/** When true, `Type(...)` constructor calls link to the Class def
|
/** When true, `Type(...)` constructor calls link to the Class def
|
||||||
* itself rather than its explicit Constructor. Swift opts in. */
|
* itself rather than its explicit Constructor. Swift opts in. */
|
||||||
readonly constructorCallTargetsClass?: boolean;
|
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 isFileLocalDef?: (def: SymbolDefinition) => boolean;
|
||||||
readonly isCallableVisibleFromCaller?: (ctx: {
|
readonly isCallableVisibleFromCaller?: (ctx: {
|
||||||
readonly callerParsed: ParsedFile;
|
readonly callerParsed: ParsedFile;
|
||||||
|
|
@ -621,8 +625,13 @@ export function emitFreeCallFallback(
|
||||||
type: 'CALLS',
|
type: 'CALLS',
|
||||||
confidence: 0.85,
|
confidence: 0.85,
|
||||||
// Match legacy DAG's reason convention so consumers that
|
// Match legacy DAG's reason convention so consumers that
|
||||||
// assert `reason === 'import-resolved'` keep working.
|
// assert `reason === 'import-resolved'` keep working. The
|
||||||
reason: fnDef.filePath !== parsed.filePath ? 'import-resolved' : 'local-call',
|
// construction-site marker is opt-in for the same reason.
|
||||||
|
reason: constructionSiteReason(
|
||||||
|
fnDef.filePath !== parsed.filePath ? 'import-resolved' : 'local-call',
|
||||||
|
site,
|
||||||
|
options.markConstructionSites,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
emitted++;
|
emitted++;
|
||||||
}
|
}
|
||||||
|
|
@ -630,6 +639,19 @@ export function emitFreeCallFallback(
|
||||||
return emitted;
|
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(
|
function siteKey(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
site: { readonly atRange: { readonly startLine: number; readonly startCol: number } },
|
site: { readonly atRange: { readonly startLine: number; readonly startCol: number } },
|
||||||
|
|
|
||||||
|
|
@ -1070,6 +1070,7 @@ export function runScopeResolution(
|
||||||
{
|
{
|
||||||
allowGlobalFallback: provider.allowGlobalFreeCallFallback === true,
|
allowGlobalFallback: provider.allowGlobalFreeCallFallback === true,
|
||||||
constructorCallTargetsClass: provider.constructorCallTargetsClass === true,
|
constructorCallTargetsClass: provider.constructorCallTargetsClass === true,
|
||||||
|
markConstructionSites: provider.markConstructionSites === true,
|
||||||
isFileLocalDef: provider.isFileLocalDef,
|
isFileLocalDef: provider.isFileLocalDef,
|
||||||
isBuiltInName: provider.languageProvider.isBuiltInName,
|
isBuiltInName: provider.languageProvider.isBuiltInName,
|
||||||
freeCallsRequireInstanceOwnership: provider.freeCallsRequireInstanceOwnership === true,
|
freeCallsRequireInstanceOwnership: provider.freeCallsRequireInstanceOwnership === true,
|
||||||
|
|
@ -1100,6 +1101,7 @@ export function runScopeResolution(
|
||||||
// both correctly emit. See the build site above for why the earlier
|
// both correctly emit. See the build site above for why the earlier
|
||||||
// allowlist could not be made safe this way.
|
// allowlist could not be made safe this way.
|
||||||
functionLocalValueDefIds,
|
functionLocalValueDefIds,
|
||||||
|
{ markConstructionSites: provider.markConstructionSites === true },
|
||||||
);
|
);
|
||||||
// Last-resort property resolution by workspace-unique name (A1/A5). Runs
|
// Last-resort property resolution by workspace-unique name (A1/A5). Runs
|
||||||
// after every precise pass and only sees what they left behind, so a
|
// 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', () => {
|
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([
|
expect(idsIn('Struct', 'Sorter.zig')).toEqual([
|
||||||
'Struct:src/Sorter.zig:Sorter',
|
'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