Skip to content

Commit f9e1f94

Browse files
authored
feat(analysis): inter-procedural return-type propagation (Phase 8.2) (#1279)
* feat(analysis): inter-procedural return-type propagation (Phase 8.2) Extends type tracking beyond single-function scope. The JS/TS extractor now records each function's declared or inferred return type in a new `returnTypeMap`, then propagates that information to variables assigned from call expressions before call-edge resolution runs. Intra-file: `const x = createUser()` resolves x's type when `createUser` has a TS return annotation or a `return new Constructor()` body. Method chains (`getService().getRepo()`) are resolved recursively up to three hops with confidence decaying 0.1 per hop (1.0 → 0.9 → 0.8 → 0.7). Cross-file: `build-edges.ts` collects unresolved call assignments from each extractor output and propagates return types from imported files before both the native and JS call-edge paths run, so both engines benefit automatically. Expected impact: +10–15 pp on caller coverage for factory patterns, builder patterns, and method chains. * fix(analysis): fix currentClass leak and annotation guard in extractReturnTypeMapWalk - Pass null as currentClass when recursing into function_declaration and method_definition bodies, preventing nested function declarations from being stored under the enclosing class name (e.g. ClassName.innerFn) - Fix annotation guard condition: `1.0 > existing.confidence` was always true (since 1.0 is maximum confidence); replace with `existing.confidence < 1.0` so the first annotation wins for duplicate function names * fix(analysis): move cross-file propagation outside transaction, export penalty constant - Move propagateReturnTypesAcrossFiles call to before computeEdgesTx opens, avoiding partial in-memory typeMap mutations if the transaction rolls back - Export PROPAGATION_HOP_PENALTY from javascript.ts and use it in build-edges.ts instead of a hardcoded 0.1 literal - Add TODO comment to typePropagationDepth config entry clarifying it is not yet wired to MAX_PROPAGATION_DEPTH (planned for Phase 8.3)
1 parent 5512efe commit f9e1f94

6 files changed

Lines changed: 466 additions & 13 deletions

File tree

src/domain/graph/builder/stages/build-edges.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import path from 'node:path';
88
import { performance } from 'node:perf_hooks';
99
import { getNodeId } from '../../../../db/index.js';
10+
import { setTypeMapEntry } from '../../../../extractors/helpers.js';
11+
import { PROPAGATION_HOP_PENALTY } from '../../../../extractors/javascript.js';
1012
import { debug } from '../../../../infrastructure/logger.js';
1113
import { loadNative } from '../../../../infrastructure/native.js';
1214
import type {
@@ -388,6 +390,64 @@ function buildImportEdgesNative(
388390
}
389391
}
390392

393+
// ── Phase 8.2: Cross-file return-type propagation ───────────────────────
394+
395+
/**
396+
* Augment each file's typeMap with return types from imported functions.
397+
*
398+
* The per-file extractor already resolves same-file call assignments (intra-file
399+
* propagation). This function handles the cross-file case: when a file imports a
400+
* function from another file and assigns its return value to a variable, we look up
401+
* the callee's return type in the source file's returnTypeMap and inject it.
402+
*
403+
* Called once before call-edge building so both the native and JS paths benefit.
404+
*/
405+
function propagateReturnTypesAcrossFiles(
406+
fileSymbols: Map<string, ExtractorOutput>,
407+
ctx: PipelineContext,
408+
rootDir: string,
409+
): void {
410+
// Index: filePath → per-file return-type map
411+
const returnTypeIndex = new Map<string, Map<string, TypeMapEntry>>();
412+
for (const [relPath, symbols] of fileSymbols) {
413+
if (symbols.returnTypeMap?.size) returnTypeIndex.set(relPath, symbols.returnTypeMap);
414+
}
415+
if (returnTypeIndex.size === 0) return;
416+
417+
// Flat global map for qualified method lookups (TypeName.methodName → entry).
418+
// Conflicts resolved by keeping the highest-confidence entry.
419+
const globalReturnTypeMap = new Map<string, TypeMapEntry>();
420+
for (const rtm of returnTypeIndex.values()) {
421+
for (const [name, entry] of rtm) {
422+
const existing = globalReturnTypeMap.get(name);
423+
if (!existing || entry.confidence > existing.confidence) globalReturnTypeMap.set(name, entry);
424+
}
425+
}
426+
427+
for (const [relPath, symbols] of fileSymbols) {
428+
if (!symbols.callAssignments?.length) continue;
429+
const importedNamesMap = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
430+
431+
for (const ca of symbols.callAssignments) {
432+
if (symbols.typeMap.has(ca.varName)) continue; // already resolved locally
433+
434+
let returnEntry: TypeMapEntry | undefined;
435+
if (ca.receiverTypeName) {
436+
returnEntry = globalReturnTypeMap.get(`${ca.receiverTypeName}.${ca.calleeName}`);
437+
} else {
438+
const importedFrom = importedNamesMap.get(ca.calleeName);
439+
if (importedFrom) returnEntry = returnTypeIndex.get(importedFrom)?.get(ca.calleeName);
440+
}
441+
442+
if (returnEntry) {
443+
const propagatedConf = returnEntry.confidence - PROPAGATION_HOP_PENALTY;
444+
if (propagatedConf > 0)
445+
setTypeMapEntry(symbols.typeMap, ca.varName, returnEntry.type, propagatedConf);
446+
}
447+
}
448+
}
449+
}
450+
391451
// ── Call edges (native engine) ──────────────────────────────────────────
392452

393453
function buildCallEdgesNative(
@@ -864,6 +924,11 @@ export async function buildEdges(ctx: PipelineContext): Promise<void> {
864924
batchInsertEdges(db, allEdgeRows);
865925
}
866926
});
927+
// Phase 8.2: Augment typeMaps with cross-file return-type propagation before
928+
// the transaction opens. This is pure in-memory mutation (no DB I/O) and must
929+
// run outside the transaction to avoid leaving ctx.fileSymbols in a partial
930+
// state if the transaction rolls back unexpectedly.
931+
propagateReturnTypesAcrossFiles(ctx.fileSymbols, ctx, ctx.rootDir);
867932
computeEdgesTx();
868933

869934
// Phase 2: Native rusqlite bulk insert (outside better-sqlite3 transaction

0 commit comments

Comments
 (0)