mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-04 02:31:36 +00:00
* 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>
230 lines
6.3 KiB
TypeScript
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 });
|
|
});
|
|
});
|