mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
refactor(SM-15): eliminate duplicated prepare logic in processCalls two-pass split
Replace the duplicated pre-pass + legacy-path code (parse → query → heritage → TypeEnv → exports) with a single preparation loop followed by a resolution loop. Both paths now share the same preparation code — the only conditional is the accumulator flush. Side benefit: globalParentMap is now fully populated before any resolution runs, improving cross-file isSubclassOf accuracy regardless of file order. Net -118 lines (226 removed, 108 added).
This commit is contained in:
parent
ddb86b35cb
commit
00e62da0f8
1 changed files with 119 additions and 237 deletions
|
|
@ -700,20 +700,17 @@ export const processCalls = async (
|
|||
const logSkipped = isVerboseIngestionEnabled();
|
||||
const skippedByLang = logSkipped ? new Map<string, number>() : null;
|
||||
|
||||
// ── Two-pass split for accumulator ordering correctness ──
|
||||
// When bindingAccumulator is present, the Phase 9 fallback in
|
||||
// verifyConstructorBindings reads from the accumulator. If we flush and
|
||||
// verify in the same per-file iteration, consumer files processed before
|
||||
// their provider files won't see the provider's bindings — causing silent
|
||||
// misses. Fix: pre-pass flushes ALL files' TypeEnv bindings into the
|
||||
// accumulator before the main loop runs verifyConstructorBindings.
|
||||
// This mirrors the worker path where all appendFile calls complete before
|
||||
// processCallsFromExtracted runs. For the sequential path (<15 files),
|
||||
// buffering per-file state is negligible.
|
||||
//
|
||||
// When bindingAccumulator is absent (legacy/Phase 14 path), the existing
|
||||
// single-pass behavior is preserved — no pre-pass, no buffering.
|
||||
interface PrePassState {
|
||||
// ── Prepare-then-resolve: single preparation loop, deferred resolution ──
|
||||
// All files are prepared (parse → query → heritage → TypeEnv) in one loop,
|
||||
// then resolved (verifyConstructorBindings → call edges) in a second loop.
|
||||
// This ensures:
|
||||
// 1. When bindingAccumulator is present, ALL files flush their TypeEnv
|
||||
// bindings before ANY verifyConstructorBindings reads — fixing the
|
||||
// consumer-before-provider ordering bug on the sequential path.
|
||||
// 2. globalParentMap is fully populated before resolution, improving
|
||||
// cross-file isSubclassOf accuracy regardless of file order.
|
||||
// For the sequential path (<15 files), buffering per-file state is negligible.
|
||||
interface PreparedFile {
|
||||
file: { path: string; content: string };
|
||||
language: SupportedLanguages;
|
||||
provider: ReturnType<typeof getProvider>;
|
||||
|
|
@ -722,240 +719,125 @@ export const processCalls = async (
|
|||
parentMap: ReadonlyMap<string, readonly string[]>;
|
||||
typeEnv: ReturnType<typeof buildTypeEnv>;
|
||||
}
|
||||
const prePassStates: PrePassState[] | undefined = bindingAccumulator ? [] : undefined;
|
||||
const prepared: PreparedFile[] = [];
|
||||
|
||||
if (bindingAccumulator) {
|
||||
// ── Pre-pass: build TypeEnv, flush to accumulator, buffer state ──
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (i % 20 === 0) await yieldToEventLoop();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (i % 20 === 0) await yieldToEventLoop();
|
||||
|
||||
const language = getLanguageFromFilename(file.path);
|
||||
if (!language) continue;
|
||||
if (!isLanguageAvailable(language)) {
|
||||
if (skippedByLang) {
|
||||
skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
|
||||
}
|
||||
continue;
|
||||
const language = getLanguageFromFilename(file.path);
|
||||
if (!language) continue;
|
||||
if (!isLanguageAvailable(language)) {
|
||||
if (skippedByLang) {
|
||||
skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1);
|
||||
}
|
||||
|
||||
const provider = getProvider(language);
|
||||
const queryStr = provider.treeSitterQueries;
|
||||
if (!queryStr) continue;
|
||||
|
||||
await loadLanguage(language, file.path);
|
||||
|
||||
let tree = astCache.get(file.path);
|
||||
if (!tree) {
|
||||
try {
|
||||
tree = parser.parse(file.content, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(file.content.length),
|
||||
});
|
||||
} catch (parseError) {
|
||||
continue;
|
||||
}
|
||||
astCache.set(file.path, tree);
|
||||
}
|
||||
|
||||
let matches;
|
||||
try {
|
||||
const lang = parser.getLanguage();
|
||||
const query = new Parser.Query(lang, queryStr);
|
||||
matches = query.matches(tree.rootNode);
|
||||
} catch (queryError) {
|
||||
console.warn(`Query error for ${file.path}:`, queryError);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract heritage for parentMap (same as main loop)
|
||||
const fileParentMap = new Map<string, string[]>();
|
||||
for (const match of matches) {
|
||||
const captureMap: Record<string, any> = {};
|
||||
match.captures.forEach((c) => (captureMap[c.name] = c.node));
|
||||
if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
|
||||
const className: string = captureMap['heritage.class'].text;
|
||||
const parentName: string = captureMap['heritage.extends'].text;
|
||||
const extendsNode = captureMap['heritage.extends'];
|
||||
const fieldDecl = extendsNode.parent;
|
||||
if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'))
|
||||
continue;
|
||||
let parents = fileParentMap.get(className);
|
||||
if (!parents) {
|
||||
parents = [];
|
||||
fileParentMap.set(className, parents);
|
||||
}
|
||||
if (!parents.includes(parentName)) parents.push(parentName);
|
||||
}
|
||||
}
|
||||
const parentMap: ReadonlyMap<string, readonly string[]> = fileParentMap;
|
||||
for (const [cls, parents] of fileParentMap) {
|
||||
let global = globalParentMap.get(cls);
|
||||
let seen = globalParentSeen.get(cls);
|
||||
if (!global) {
|
||||
global = [];
|
||||
globalParentMap.set(cls, global);
|
||||
}
|
||||
if (!seen) {
|
||||
seen = new Set();
|
||||
globalParentSeen.set(cls, seen);
|
||||
}
|
||||
for (const p of parents) {
|
||||
if (!seen.has(p)) {
|
||||
seen.add(p);
|
||||
global.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const importedBindings = importedBindingsMap?.get(file.path);
|
||||
const importedReturnTypes = importedReturnTypesMap?.get(file.path);
|
||||
const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
|
||||
const typeEnv = buildTypeEnv(tree, language, {
|
||||
symbolTable: ctx.symbols,
|
||||
parentMap,
|
||||
importedBindings,
|
||||
importedReturnTypes,
|
||||
importedRawReturnTypes,
|
||||
enclosingFunctionFinder: provider?.enclosingFunctionFinder,
|
||||
extractFunctionName: provider?.methodExtractor?.extractFunctionName,
|
||||
});
|
||||
if (typeEnv && exportedTypeMap) {
|
||||
const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
|
||||
if (fileExports) exportedTypeMap.set(file.path, fileExports);
|
||||
}
|
||||
typeEnv.flush(file.path, bindingAccumulator);
|
||||
|
||||
prePassStates!.push({ file, language, provider, tree, matches, parentMap, typeEnv });
|
||||
continue;
|
||||
}
|
||||
|
||||
const provider = getProvider(language);
|
||||
const queryStr = provider.treeSitterQueries;
|
||||
if (!queryStr) continue;
|
||||
|
||||
await loadLanguage(language, file.path);
|
||||
|
||||
let tree = astCache.get(file.path);
|
||||
if (!tree) {
|
||||
try {
|
||||
tree = parser.parse(file.content, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(file.content.length),
|
||||
});
|
||||
} catch (parseError) {
|
||||
continue;
|
||||
}
|
||||
astCache.set(file.path, tree);
|
||||
}
|
||||
|
||||
let matches;
|
||||
try {
|
||||
const lang = parser.getLanguage();
|
||||
const query = new Parser.Query(lang, queryStr);
|
||||
matches = query.matches(tree.rootNode);
|
||||
} catch (queryError) {
|
||||
console.warn(`Query error for ${file.path}:`, queryError);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract heritage from query matches to build parentMap for buildTypeEnv.
|
||||
// Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs.
|
||||
const fileParentMap = new Map<string, string[]>();
|
||||
for (const match of matches) {
|
||||
const captureMap: Record<string, any> = {};
|
||||
match.captures.forEach((c) => (captureMap[c.name] = c.node));
|
||||
if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
|
||||
const className: string = captureMap['heritage.class'].text;
|
||||
const parentName: string = captureMap['heritage.extends'].text;
|
||||
const extendsNode = captureMap['heritage.extends'];
|
||||
const fieldDecl = extendsNode.parent;
|
||||
if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'))
|
||||
continue;
|
||||
let parents = fileParentMap.get(className);
|
||||
if (!parents) {
|
||||
parents = [];
|
||||
fileParentMap.set(className, parents);
|
||||
}
|
||||
if (!parents.includes(parentName)) parents.push(parentName);
|
||||
}
|
||||
}
|
||||
const parentMap: ReadonlyMap<string, readonly string[]> = fileParentMap;
|
||||
// Merge per-file heritage into globalParentMap for cross-file isSubclassOf lookups.
|
||||
for (const [cls, parents] of fileParentMap) {
|
||||
let global = globalParentMap.get(cls);
|
||||
let seen = globalParentSeen.get(cls);
|
||||
if (!global) {
|
||||
global = [];
|
||||
globalParentMap.set(cls, global);
|
||||
}
|
||||
if (!seen) {
|
||||
seen = new Set();
|
||||
globalParentSeen.set(cls, seen);
|
||||
}
|
||||
for (const p of parents) {
|
||||
if (!seen.has(p)) {
|
||||
seen.add(p);
|
||||
global.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const importedBindings = importedBindingsMap?.get(file.path);
|
||||
const importedReturnTypes = importedReturnTypesMap?.get(file.path);
|
||||
const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
|
||||
const typeEnv = buildTypeEnv(tree, language, {
|
||||
symbolTable: ctx.symbols,
|
||||
parentMap,
|
||||
importedBindings,
|
||||
importedReturnTypes,
|
||||
importedRawReturnTypes,
|
||||
enclosingFunctionFinder: provider?.enclosingFunctionFinder,
|
||||
extractFunctionName: provider?.methodExtractor?.extractFunctionName,
|
||||
});
|
||||
if (typeEnv && exportedTypeMap) {
|
||||
const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
|
||||
if (fileExports) exportedTypeMap.set(file.path, fileExports);
|
||||
}
|
||||
if (bindingAccumulator) {
|
||||
typeEnv.flush(file.path, bindingAccumulator);
|
||||
}
|
||||
|
||||
prepared.push({ file, language, provider, tree, matches, parentMap, typeEnv });
|
||||
}
|
||||
|
||||
// ── Main loop: resolve calls (and optionally build TypeEnv if no pre-pass) ──
|
||||
const loopSource = prePassStates ?? files;
|
||||
for (let i = 0; i < loopSource.length; i++) {
|
||||
const isPrePassed = prePassStates !== undefined;
|
||||
const entry = loopSource[i];
|
||||
const file = isPrePassed
|
||||
? (entry as PrePassState).file
|
||||
: (entry as { path: string; content: string });
|
||||
// ── Resolution loop: verify constructor bindings and resolve calls ──
|
||||
// The accumulator (if present) is now fully populated from the preparation
|
||||
// loop above, so verifyConstructorBindings sees all provider bindings
|
||||
// regardless of file processing order.
|
||||
for (let i = 0; i < prepared.length; i++) {
|
||||
const { file, language, provider, tree, matches, parentMap, typeEnv } = prepared[i];
|
||||
|
||||
enclosingFnExtractCache.clear();
|
||||
onProgress?.(i + 1, files.length);
|
||||
if (i % 20 === 0) await yieldToEventLoop();
|
||||
|
||||
let language: SupportedLanguages;
|
||||
let provider: ReturnType<typeof getProvider>;
|
||||
let tree: ReturnType<typeof parser.parse>;
|
||||
let matches: ReturnType<Parser.Query['matches']>;
|
||||
let typeEnv: ReturnType<typeof buildTypeEnv>;
|
||||
let parentMap: ReadonlyMap<string, readonly string[]>;
|
||||
|
||||
if (isPrePassed) {
|
||||
// Reuse state from pre-pass — TypeEnv and flush already done
|
||||
const state = entry as PrePassState;
|
||||
language = state.language;
|
||||
provider = state.provider;
|
||||
tree = state.tree;
|
||||
matches = state.matches;
|
||||
typeEnv = state.typeEnv;
|
||||
parentMap = state.parentMap;
|
||||
} else {
|
||||
// Legacy single-pass path (no bindingAccumulator)
|
||||
const lang = getLanguageFromFilename(file.path);
|
||||
if (!lang) continue;
|
||||
if (!isLanguageAvailable(lang)) {
|
||||
if (skippedByLang) {
|
||||
skippedByLang.set(lang, (skippedByLang.get(lang) ?? 0) + 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
language = lang;
|
||||
|
||||
provider = getProvider(language);
|
||||
const queryStr = provider.treeSitterQueries;
|
||||
if (!queryStr) continue;
|
||||
|
||||
await loadLanguage(language, file.path);
|
||||
|
||||
tree = astCache.get(file.path)!;
|
||||
if (!tree) {
|
||||
try {
|
||||
tree = parser.parse(file.content, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(file.content.length),
|
||||
});
|
||||
} catch (parseError) {
|
||||
continue;
|
||||
}
|
||||
astCache.set(file.path, tree);
|
||||
}
|
||||
|
||||
try {
|
||||
const parseLang = parser.getLanguage();
|
||||
const query = new Parser.Query(parseLang, queryStr);
|
||||
matches = query.matches(tree.rootNode);
|
||||
} catch (queryError) {
|
||||
console.warn(`Query error for ${file.path}:`, queryError);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Heritage extraction (same as pre-pass, only runs in legacy path)
|
||||
const fileParentMap = new Map<string, string[]>();
|
||||
for (const match of matches) {
|
||||
const captureMap: Record<string, any> = {};
|
||||
match.captures.forEach((c) => (captureMap[c.name] = c.node));
|
||||
if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
|
||||
const className: string = captureMap['heritage.class'].text;
|
||||
const parentName: string = captureMap['heritage.extends'].text;
|
||||
const extendsNode = captureMap['heritage.extends'];
|
||||
const fieldDecl = extendsNode.parent;
|
||||
if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'))
|
||||
continue;
|
||||
let parents = fileParentMap.get(className);
|
||||
if (!parents) {
|
||||
parents = [];
|
||||
fileParentMap.set(className, parents);
|
||||
}
|
||||
if (!parents.includes(parentName)) parents.push(parentName);
|
||||
}
|
||||
}
|
||||
for (const [cls, parents] of fileParentMap) {
|
||||
let global = globalParentMap.get(cls);
|
||||
let seen = globalParentSeen.get(cls);
|
||||
if (!global) {
|
||||
global = [];
|
||||
globalParentMap.set(cls, global);
|
||||
}
|
||||
if (!seen) {
|
||||
seen = new Set();
|
||||
globalParentSeen.set(cls, seen);
|
||||
}
|
||||
for (const p of parents) {
|
||||
if (!seen.has(p)) {
|
||||
seen.add(p);
|
||||
global.push(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parentMap = fileParentMap;
|
||||
|
||||
const importedBindings = importedBindingsMap?.get(file.path);
|
||||
const importedReturnTypes = importedReturnTypesMap?.get(file.path);
|
||||
const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
|
||||
typeEnv = buildTypeEnv(tree, language, {
|
||||
symbolTable: ctx.symbols,
|
||||
parentMap,
|
||||
importedBindings,
|
||||
importedReturnTypes,
|
||||
importedRawReturnTypes,
|
||||
enclosingFunctionFinder: provider?.enclosingFunctionFinder,
|
||||
extractFunctionName: provider?.methodExtractor?.extractFunctionName,
|
||||
});
|
||||
if (typeEnv && exportedTypeMap) {
|
||||
const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
|
||||
if (fileExports) exportedTypeMap.set(file.path, fileExports);
|
||||
}
|
||||
}
|
||||
|
||||
const callRouter = provider.callRouter;
|
||||
|
||||
const verifiedReceivers =
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue