GitNexus/gitnexus/test/unit/scope-resolution/callable-value-flow-worklist.test.ts
Gergő Magyar 52924ef12c
perf(emit): drop six equivalent redundancies on Java-scale index path (#3129)
* 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>
2026-09-01 11:22:55 +00:00

230 lines
6.3 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
buildDefIndex,
buildMethodDispatchIndex,
buildModuleScopeIndex,
buildQualifiedNameIndex,
buildScopeTree,
type BindingRef,
type CallableFlowOperand,
type ParsedFile,
type Range,
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 { createCalleeIdAccumulator } from '../../../src/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js';
import {
emitCallableValueFlow,
collectDeferredIndirectCollection,
} from '../../../src/core/ingestion/scope-resolution/passes/callable-value-flow.js';
const FILE = 'chain.ts';
const MODULE = 'scope:module' as ScopeId;
const ENTRY_SCOPE = 'scope:entry' as ScopeId;
const range = (line: number): Range => ({
startLine: line,
startCol: 0,
endLine: line,
endCol: 8,
});
const targetDef: SymbolDefinition = {
nodeId: 'def:chain.ts#1:0:Function:target',
filePath: FILE,
type: 'Function',
qualifiedName: 'target',
parameterCount: 0,
};
const entryDef: SymbolDefinition = {
nodeId: 'def:chain.ts#2:0:Function:entry',
filePath: FILE,
type: 'Function',
qualifiedName: 'entry',
parameterCount: 0,
};
function scope(
id: ScopeId,
parent: ScopeId | null,
kind: Scope['kind'],
ownedDefs: readonly SymbolDefinition[],
bindings: ReadonlyMap<string, readonly BindingRef[]> = new Map(),
): Scope {
return {
id,
parent,
kind,
range: range(1),
filePath: FILE,
bindings,
ownedDefs,
imports: [],
typeBindings: new Map(),
};
}
function indexes(scopes: readonly Scope[]): ScopeResolutionIndexes {
const defs = [targetDef, entryDef];
return {
scopeTree: buildScopeTree([...scopes]),
defs: buildDefIndex(defs),
qualifiedNames: buildQualifiedNameIndex(defs),
moduleScopes: buildModuleScopeIndex([{ filePath: FILE, moduleScopeId: MODULE }]),
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: 1,
totalEdges: 0,
linkedEdges: 0,
unresolvedEdges: 0,
sccCount: 1,
largestSccSize: 1,
},
};
}
function operand(name: string, line: number): CallableFlowOperand {
return {
name,
inScope: ENTRY_SCOPE,
atRange: range(line),
indirection: 0,
addressOf: false,
expressionKind: 'binding',
};
}
function runReverseChain(length: number) {
const moduleBindings = new Map<string, readonly BindingRef[]>([
['target', [{ def: targetDef }]],
['entry', [{ def: entryDef }]],
]);
const scopes = [
scope(MODULE, null, 'Module', [targetDef, entryDef], moduleBindings),
scope(ENTRY_SCOPE, MODULE, 'Function', [entryDef]),
];
const callSite = range(length + 10);
const copies = Array.from({ length }, (_, index) => index)
.reverse()
.map((index) => ({
kind: 'copy' as const,
source: operand(`value${index}`, index + 3),
destination: operand(`value${index + 1}`, index + 3),
}));
const parsed: ParsedFile = {
filePath: FILE,
moduleScope: MODULE,
scopes,
parsedImports: [],
localDefs: [targetDef, entryDef],
referenceSites: [
{
name: `value${length}`,
atRange: callSite,
inScope: ENTRY_SCOPE,
kind: 'call',
callForm: 'free',
},
],
callableFlowSites: [
{
kind: 'seed',
destination: operand('value0', 3),
targetName: 'target',
targetRange: range(1),
},
...copies,
{
kind: 'invoke',
callSite,
inScope: ENTRY_SCOPE,
callee: operand(`value${length}`, callSite.startLine),
invocationKind: 'indirect',
arity: 0,
},
],
};
const graph = createKnowledgeGraph();
graph.addNode({
id: 'Function:chain.ts:target',
label: 'Function',
properties: { name: 'target', qualifiedName: 'target', filePath: FILE },
});
graph.addNode({
id: 'Function:chain.ts:entry',
label: 'Function',
properties: { name: 'entry', qualifiedName: 'entry', filePath: FILE },
});
const result = emitCallableValueFlow({
graph,
scopes: indexes(scopes),
parsedFiles: [parsed],
nodeLookup: buildGraphNodeLookup(graph),
calleeIds: createCalleeIdAccumulator(),
language: 'typescript',
});
return { graph, result };
}
describe('callable-value-flow dependency worklist', () => {
it('scales linearly for reverse-ordered chains and resolves the terminal call', () => {
const n = runReverseChain(128);
const twoN = runReverseChain(256);
expect(n.result.resolvedInvokes).toBe(1);
expect(twoN.result.resolvedInvokes).toBe(1);
expect(n.result.emitted).toBe(1);
expect(twoN.result.emitted).toBe(1);
expect(twoN.result.iterations).toBeLessThanOrEqual(n.result.iterations * 2 + 4);
expect(twoN.result.iterations).toBeLessThan(1_024);
expect(
[...twoN.graph.iterRelationshipsByType('CALLS')].map(
(relationship) => twoN.graph.getNode(relationship.targetId)?.properties.name,
),
).toEqual(['target']);
});
});
describe('collectDeferredIndirectCollection', () => {
it('fills call signatures from the first referenceSites walk', () => {
const parsed: ParsedFile = {
filePath: FILE,
moduleScope: MODULE,
scopes: [],
parsedImports: [],
localDefs: [],
referenceSites: [
{
name: 'target',
kind: 'call',
callForm: 'free',
atRange: range(1),
inScope: MODULE,
arity: 0,
},
],
callableFlowSites: [],
};
const collected = collectDeferredIndirectCollection([parsed]);
expect(collected.callSignaturesBySite.get(`${FILE}:1:0`)).toEqual({ parameterCount: 0 });
});
});