@optave/codegraph 3.11.2 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/migrations.js +7 -0
- package/dist/db/migrations.js.map +1 -1
- package/dist/domain/analysis/module-map.d.ts +2 -0
- package/dist/domain/analysis/module-map.d.ts.map +1 -1
- package/dist/domain/analysis/module-map.js +24 -2
- package/dist/domain/analysis/module-map.js.map +1 -1
- package/dist/domain/graph/builder/call-resolver.d.ts +4 -2
- package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -1
- package/dist/domain/graph/builder/call-resolver.js +170 -8
- package/dist/domain/graph/builder/call-resolver.js.map +1 -1
- package/dist/domain/graph/builder/cha.d.ts +61 -0
- package/dist/domain/graph/builder/cha.d.ts.map +1 -0
- package/dist/domain/graph/builder/cha.js +143 -0
- package/dist/domain/graph/builder/cha.js.map +1 -0
- package/dist/domain/graph/builder/context.d.ts +3 -0
- package/dist/domain/graph/builder/context.d.ts.map +1 -1
- package/dist/domain/graph/builder/context.js +2 -0
- package/dist/domain/graph/builder/context.js.map +1 -1
- package/dist/domain/graph/builder/helpers.d.ts +17 -1
- package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
- package/dist/domain/graph/builder/helpers.js +159 -5
- package/dist/domain/graph/builder/helpers.js.map +1 -1
- package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
- package/dist/domain/graph/builder/incremental.js +73 -1
- package/dist/domain/graph/builder/incremental.js.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.d.ts +2 -0
- package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/build-edges.js +926 -26
- package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/detect-changes.js +2 -1
- package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/native-orchestrator.js +501 -14
- package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
- package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
- package/dist/domain/graph/builder/stages/resolve-imports.js +9 -0
- package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
- package/dist/domain/graph/journal.js +1 -1
- package/dist/domain/graph/journal.js.map +1 -1
- package/dist/domain/graph/resolver/points-to.d.ts +53 -0
- package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
- package/dist/domain/graph/resolver/points-to.js +213 -0
- package/dist/domain/graph/resolver/points-to.js.map +1 -0
- package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
- package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
- package/dist/domain/graph/resolver/ts-resolver.js +476 -0
- package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
- package/dist/domain/parser.d.ts +10 -1
- package/dist/domain/parser.d.ts.map +1 -1
- package/dist/domain/parser.js +39 -7
- package/dist/domain/parser.js.map +1 -1
- package/dist/domain/wasm-worker-entry.js +25 -0
- package/dist/domain/wasm-worker-entry.js.map +1 -1
- package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
- package/dist/domain/wasm-worker-pool.js +32 -0
- package/dist/domain/wasm-worker-pool.js.map +1 -1
- package/dist/domain/wasm-worker-protocol.d.ts +14 -1
- package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
- package/dist/extractors/c.js +3 -3
- package/dist/extractors/c.js.map +1 -1
- package/dist/extractors/clojure.js +1 -1
- package/dist/extractors/clojure.js.map +1 -1
- package/dist/extractors/cpp.js +3 -3
- package/dist/extractors/cpp.js.map +1 -1
- package/dist/extractors/csharp.d.ts.map +1 -1
- package/dist/extractors/csharp.js +37 -8
- package/dist/extractors/csharp.js.map +1 -1
- package/dist/extractors/cuda.js +3 -3
- package/dist/extractors/cuda.js.map +1 -1
- package/dist/extractors/elixir.js +6 -6
- package/dist/extractors/elixir.js.map +1 -1
- package/dist/extractors/fsharp.js +1 -1
- package/dist/extractors/fsharp.js.map +1 -1
- package/dist/extractors/go.js +5 -5
- package/dist/extractors/go.js.map +1 -1
- package/dist/extractors/haskell.js +1 -1
- package/dist/extractors/haskell.js.map +1 -1
- package/dist/extractors/java.js +2 -2
- package/dist/extractors/java.js.map +1 -1
- package/dist/extractors/javascript.d.ts +2 -0
- package/dist/extractors/javascript.d.ts.map +1 -1
- package/dist/extractors/javascript.js +1674 -64
- package/dist/extractors/javascript.js.map +1 -1
- package/dist/extractors/kotlin.js +5 -5
- package/dist/extractors/kotlin.js.map +1 -1
- package/dist/extractors/lua.js +1 -1
- package/dist/extractors/lua.js.map +1 -1
- package/dist/extractors/objc.js +3 -3
- package/dist/extractors/objc.js.map +1 -1
- package/dist/extractors/ocaml.js +1 -1
- package/dist/extractors/ocaml.js.map +1 -1
- package/dist/extractors/php.js +2 -2
- package/dist/extractors/php.js.map +1 -1
- package/dist/extractors/python.js +7 -7
- package/dist/extractors/python.js.map +1 -1
- package/dist/extractors/ruby.js +2 -2
- package/dist/extractors/ruby.js.map +1 -1
- package/dist/extractors/scala.js +1 -1
- package/dist/extractors/scala.js.map +1 -1
- package/dist/extractors/solidity.js +1 -1
- package/dist/extractors/solidity.js.map +1 -1
- package/dist/extractors/swift.js +4 -4
- package/dist/extractors/swift.js.map +1 -1
- package/dist/extractors/zig.js +4 -4
- package/dist/extractors/zig.js.map +1 -1
- package/dist/infrastructure/config.d.ts +10 -0
- package/dist/infrastructure/config.d.ts.map +1 -1
- package/dist/infrastructure/config.js +15 -0
- package/dist/infrastructure/config.js.map +1 -1
- package/dist/infrastructure/native.d.ts +11 -0
- package/dist/infrastructure/native.d.ts.map +1 -1
- package/dist/infrastructure/native.js +78 -5
- package/dist/infrastructure/native.js.map +1 -1
- package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
- package/dist/presentation/queries-cli/overview.js +5 -0
- package/dist/presentation/queries-cli/overview.js.map +1 -1
- package/dist/types.d.ts +184 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/db/migrations.ts +7 -0
- package/src/domain/analysis/module-map.ts +29 -1
- package/src/domain/graph/builder/call-resolver.ts +177 -7
- package/src/domain/graph/builder/cha.ts +175 -0
- package/src/domain/graph/builder/context.ts +3 -0
- package/src/domain/graph/builder/helpers.ts +175 -5
- package/src/domain/graph/builder/incremental.ts +79 -1
- package/src/domain/graph/builder/stages/build-edges.ts +1128 -24
- package/src/domain/graph/builder/stages/detect-changes.ts +3 -1
- package/src/domain/graph/builder/stages/native-orchestrator.ts +583 -20
- package/src/domain/graph/builder/stages/resolve-imports.ts +14 -0
- package/src/domain/graph/journal.ts +1 -1
- package/src/domain/graph/resolver/points-to.ts +254 -0
- package/src/domain/graph/resolver/ts-resolver.ts +536 -0
- package/src/domain/parser.ts +43 -5
- package/src/domain/wasm-worker-entry.ts +25 -0
- package/src/domain/wasm-worker-pool.ts +21 -0
- package/src/domain/wasm-worker-protocol.ts +14 -0
- package/src/extractors/c.ts +3 -3
- package/src/extractors/clojure.ts +1 -1
- package/src/extractors/cpp.ts +3 -3
- package/src/extractors/csharp.ts +33 -9
- package/src/extractors/cuda.ts +3 -3
- package/src/extractors/elixir.ts +6 -6
- package/src/extractors/fsharp.ts +1 -1
- package/src/extractors/go.ts +5 -5
- package/src/extractors/haskell.ts +1 -1
- package/src/extractors/java.ts +2 -2
- package/src/extractors/javascript.ts +1802 -66
- package/src/extractors/kotlin.ts +5 -5
- package/src/extractors/lua.ts +1 -1
- package/src/extractors/objc.ts +3 -3
- package/src/extractors/ocaml.ts +1 -1
- package/src/extractors/php.ts +2 -2
- package/src/extractors/python.ts +7 -7
- package/src/extractors/ruby.ts +2 -2
- package/src/extractors/scala.ts +1 -1
- package/src/extractors/solidity.ts +1 -1
- package/src/extractors/swift.ts +4 -4
- package/src/extractors/zig.ts +4 -4
- package/src/infrastructure/config.ts +15 -0
- package/src/infrastructure/native.ts +87 -5
- package/src/presentation/queries-cli/overview.ts +15 -1
- package/src/types.ts +194 -0
|
@@ -7,12 +7,19 @@
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { performance } from 'node:perf_hooks';
|
|
9
9
|
import { getNodeId } from '../../../../db/index.js';
|
|
10
|
+
import { setTypeMapEntry } from '../../../../extractors/helpers.js';
|
|
11
|
+
import { PROPAGATION_HOP_PENALTY } from '../../../../extractors/javascript.js';
|
|
10
12
|
import { debug } from '../../../../infrastructure/logger.js';
|
|
11
13
|
import { loadNative } from '../../../../infrastructure/native.js';
|
|
12
14
|
import { computeConfidence } from '../../resolve.js';
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
15
|
+
import { buildPointsToMap, resolveViaPointsTo } from '../../resolver/points-to.js';
|
|
16
|
+
import { enrichTypeMapWithTsc } from '../../resolver/ts-resolver.js';
|
|
17
|
+
import { findCaller, isModuleScopedLanguage, resolveCallTargets, resolveReceiverEdge, } from '../call-resolver.js';
|
|
18
|
+
import { buildChaContext, resolveChaTargets, resolveThisDispatch } from '../cha.js';
|
|
19
|
+
import { BUILTIN_RECEIVERS, batchInsertEdges, runChaPostPass } from '../helpers.js';
|
|
20
|
+
import { getResolved, isBarrelFile, resolveBarrelExportCached } from './resolve-imports.js';
|
|
21
|
+
/** Phase 8.5: confidence penalty applied to CHA-dispatch edges. */
|
|
22
|
+
export const CHA_DISPATCH_PENALTY = 0.1;
|
|
16
23
|
// ── Node lookup setup ───────────────────────────────────────────────────
|
|
17
24
|
function makeGetNodeIdStmt(db) {
|
|
18
25
|
return {
|
|
@@ -59,13 +66,13 @@ function emitTypeOnlySymbolEdges(ctx, imp, resolvedPath, fileNodeId, allEdgeRows
|
|
|
59
66
|
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
60
67
|
let targetFile = resolvedPath;
|
|
61
68
|
if (isBarrelFile(ctx, resolvedPath)) {
|
|
62
|
-
const actual =
|
|
69
|
+
const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
|
|
63
70
|
if (actual)
|
|
64
71
|
targetFile = actual;
|
|
65
72
|
}
|
|
66
73
|
const candidates = ctx.nodesByNameAndFile.get(`${cleanName}|${targetFile}`);
|
|
67
74
|
if (candidates && candidates.length > 0) {
|
|
68
|
-
allEdgeRows.push([fileNodeId, candidates[0].id, 'imports-type', 1.0, 0]);
|
|
75
|
+
allEdgeRows.push([fileNodeId, candidates[0].id, 'imports-type', 1.0, 0, null]);
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
78
|
}
|
|
@@ -79,7 +86,7 @@ function emitEdgesForImport(ctx, imp, fileNodeId, relPath, getNodeIdStmt, allEdg
|
|
|
79
86
|
if (!targetRow)
|
|
80
87
|
return;
|
|
81
88
|
const edgeKind = importEdgeKind(imp);
|
|
82
|
-
allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0]);
|
|
89
|
+
allEdgeRows.push([fileNodeId, targetRow.id, edgeKind, 1.0, 0, null]);
|
|
83
90
|
if (imp.typeOnly) {
|
|
84
91
|
emitTypeOnlySymbolEdges(ctx, imp, resolvedPath, fileNodeId, allEdgeRows);
|
|
85
92
|
}
|
|
@@ -107,7 +114,7 @@ function buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeI
|
|
|
107
114
|
const resolvedSources = new Set();
|
|
108
115
|
for (const name of imp.names) {
|
|
109
116
|
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
110
|
-
const actualSource =
|
|
117
|
+
const actualSource = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
|
|
111
118
|
if (actualSource && actualSource !== resolvedPath && !resolvedSources.has(actualSource)) {
|
|
112
119
|
resolvedSources.add(actualSource);
|
|
113
120
|
const actualRow = getNodeIdStmt.get(actualSource, 'file', actualSource, 0);
|
|
@@ -117,7 +124,7 @@ function buildBarrelEdges(ctx, imp, resolvedPath, fileNodeId, edgeKind, getNodeI
|
|
|
117
124
|
: edgeKind === 'dynamic-imports'
|
|
118
125
|
? 'dynamic-imports'
|
|
119
126
|
: 'imports';
|
|
120
|
-
edgeRows.push([fileNodeId, actualRow.id, kind, 0.9, 0]);
|
|
127
|
+
edgeRows.push([fileNodeId, actualRow.id, kind, 0.9, 0, null]);
|
|
121
128
|
}
|
|
122
129
|
}
|
|
123
130
|
}
|
|
@@ -232,7 +239,66 @@ function buildImportEdgesNative(ctx, getNodeIdStmt, allEdgeRows, native) {
|
|
|
232
239
|
const symbolNodes = collectSymbolNodes(ctx);
|
|
233
240
|
const nativeEdges = native.buildImportEdges(files, resolvedImports, fileReexports, registry.ids, barrelFiles, ctx.rootDir, symbolNodes);
|
|
234
241
|
for (const e of nativeEdges) {
|
|
235
|
-
allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic]);
|
|
242
|
+
allEdgeRows.push([e.sourceId, e.targetId, e.kind, e.confidence, e.dynamic, null]);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// ── Phase 8.2: Cross-file return-type propagation ───────────────────────
|
|
246
|
+
/**
|
|
247
|
+
* Augment each file's typeMap with return types from imported functions.
|
|
248
|
+
*
|
|
249
|
+
* The per-file extractor already resolves same-file call assignments (intra-file
|
|
250
|
+
* propagation). This function handles the cross-file case: when a file imports a
|
|
251
|
+
* function from another file and assigns its return value to a variable, we look up
|
|
252
|
+
* the callee's return type in the source file's returnTypeMap and inject it.
|
|
253
|
+
*
|
|
254
|
+
* Called once before call-edge building so both the native and JS paths benefit.
|
|
255
|
+
*/
|
|
256
|
+
function propagateReturnTypesAcrossFiles(fileSymbols, ctx, rootDir) {
|
|
257
|
+
// Index: filePath → per-file return-type map
|
|
258
|
+
const returnTypeIndex = new Map();
|
|
259
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
260
|
+
if (symbols.returnTypeMap?.size)
|
|
261
|
+
returnTypeIndex.set(relPath, symbols.returnTypeMap);
|
|
262
|
+
}
|
|
263
|
+
if (returnTypeIndex.size === 0)
|
|
264
|
+
return;
|
|
265
|
+
// Flat global map for qualified method lookups (TypeName.methodName → entry).
|
|
266
|
+
// Conflicts resolved by keeping the highest-confidence entry.
|
|
267
|
+
const globalReturnTypeMap = new Map();
|
|
268
|
+
for (const rtm of returnTypeIndex.values()) {
|
|
269
|
+
for (const [name, entry] of rtm) {
|
|
270
|
+
const existing = globalReturnTypeMap.get(name);
|
|
271
|
+
if (!existing || entry.confidence > existing.confidence)
|
|
272
|
+
globalReturnTypeMap.set(name, entry);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
276
|
+
if (!symbols.callAssignments?.length)
|
|
277
|
+
continue;
|
|
278
|
+
// Phase 8.4 side-effect: buildImportedNamesMap now traces through barrel
|
|
279
|
+
// files (traceBarrel), so `importedFrom` resolves to the leaf definition
|
|
280
|
+
// file rather than the barrel. This means returnTypeIndex.get(importedFrom)
|
|
281
|
+
// now finds entries it previously missed, improving cross-file return-type
|
|
282
|
+
// propagation through re-export chains (Phase 8.2 improvement).
|
|
283
|
+
const importedNamesMap = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
|
|
284
|
+
for (const ca of symbols.callAssignments) {
|
|
285
|
+
if (symbols.typeMap.has(ca.varName))
|
|
286
|
+
continue; // already resolved locally
|
|
287
|
+
let returnEntry;
|
|
288
|
+
if (ca.receiverTypeName) {
|
|
289
|
+
returnEntry = globalReturnTypeMap.get(`${ca.receiverTypeName}.${ca.calleeName}`);
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const importedFrom = importedNamesMap.get(ca.calleeName);
|
|
293
|
+
if (importedFrom)
|
|
294
|
+
returnEntry = returnTypeIndex.get(importedFrom)?.get(ca.calleeName);
|
|
295
|
+
}
|
|
296
|
+
if (returnEntry) {
|
|
297
|
+
const propagatedConf = returnEntry.confidence - PROPAGATION_HOP_PENALTY;
|
|
298
|
+
if (propagatedConf > 0)
|
|
299
|
+
setTypeMapEntry(symbols.typeMap, ca.varName, returnEntry.type, propagatedConf);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
236
302
|
}
|
|
237
303
|
}
|
|
238
304
|
// ── Call edges (native engine) ──────────────────────────────────────────
|
|
@@ -281,13 +347,432 @@ function buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodes, native)
|
|
|
281
347
|
importedNames,
|
|
282
348
|
classes: symbols.classes,
|
|
283
349
|
typeMap,
|
|
350
|
+
fnRefBindings: symbols.fnRefBindings?.length ? symbols.fnRefBindings : undefined,
|
|
284
351
|
});
|
|
285
352
|
}
|
|
286
353
|
const nativeEdges = native.buildCallEdges(nativeFiles, allNodes, [
|
|
287
354
|
...BUILTIN_RECEIVERS,
|
|
288
355
|
]);
|
|
289
356
|
for (const e of nativeEdges) {
|
|
290
|
-
allEdgeRows.push([
|
|
357
|
+
allEdgeRows.push([
|
|
358
|
+
e.sourceId,
|
|
359
|
+
e.targetId,
|
|
360
|
+
e.kind,
|
|
361
|
+
e.confidence,
|
|
362
|
+
e.dynamic,
|
|
363
|
+
e.kind === 'calls' ? 'ts-native' : null,
|
|
364
|
+
]);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Phase 8.3c pts post-pass for the native call-edge path.
|
|
369
|
+
*
|
|
370
|
+
* The native Rust engine builds call edges without knowledge of paramBindings,
|
|
371
|
+
* so `fn()` calls inside higher-order functions are not resolved to their
|
|
372
|
+
* concrete targets. This JS post-pass runs after the native edge pass and adds
|
|
373
|
+
* only the parameter-flow pts edges that the native engine missed.
|
|
374
|
+
*
|
|
375
|
+
* To avoid duplicating edges already emitted by the native engine, the current
|
|
376
|
+
* allEdgeRows snapshot is used to seed a seenByPair set before processing each
|
|
377
|
+
* file.
|
|
378
|
+
*/
|
|
379
|
+
function buildParamFlowPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup) {
|
|
380
|
+
// Only process files that actually have paramBindings (avoid useless work).
|
|
381
|
+
const filesWithParams = [...ctx.fileSymbols].filter(([, symbols]) => symbols.paramBindings && symbols.paramBindings.length > 0);
|
|
382
|
+
if (filesWithParams.length === 0)
|
|
383
|
+
return;
|
|
384
|
+
// Seed seenByPair from the existing rows so we don't duplicate native edges.
|
|
385
|
+
// This is O(|allEdgeRows|) once per post-pass, which is acceptable.
|
|
386
|
+
const seenByPair = new Set();
|
|
387
|
+
for (const [srcId, tgtId] of allEdgeRows) {
|
|
388
|
+
seenByPair.add(`${srcId}|${tgtId}`);
|
|
389
|
+
}
|
|
390
|
+
const { barrelOnlyFiles, rootDir } = ctx;
|
|
391
|
+
const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
|
|
392
|
+
for (const [relPath, symbols] of filesWithParams) {
|
|
393
|
+
if (barrelOnlyFiles.has(relPath))
|
|
394
|
+
continue;
|
|
395
|
+
const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
396
|
+
if (!fileNodeRow)
|
|
397
|
+
continue;
|
|
398
|
+
const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
|
|
399
|
+
const typeMap = symbols.typeMap || new Map();
|
|
400
|
+
const ptsMap = buildPointsToMapForFile(symbols, importedNames);
|
|
401
|
+
if (!ptsMap)
|
|
402
|
+
continue;
|
|
403
|
+
for (const call of symbols.calls) {
|
|
404
|
+
if (call.receiver || call.dynamic)
|
|
405
|
+
continue; // pts post-pass handles only param-flow (non-dynamic)
|
|
406
|
+
const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
|
|
407
|
+
const scopedKey = caller.callerName != null ? `${caller.callerName}::${call.name}` : null;
|
|
408
|
+
if (!scopedKey || !ptsMap.has(scopedKey))
|
|
409
|
+
continue;
|
|
410
|
+
// Only resolve calls that had no direct targets (same guard as buildFileCallEdges).
|
|
411
|
+
const { targets } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap);
|
|
412
|
+
if (targets.length > 0)
|
|
413
|
+
continue;
|
|
414
|
+
for (const alias of resolveViaPointsTo(scopedKey, ptsMap)) {
|
|
415
|
+
const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
|
|
416
|
+
for (const t of aliasTargets) {
|
|
417
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
418
|
+
if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
|
|
419
|
+
const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
|
|
420
|
+
if (conf > 0) {
|
|
421
|
+
seenByPair.add(edgeKey);
|
|
422
|
+
allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* bind/alias pts post-pass for the native call-edge path.
|
|
432
|
+
*
|
|
433
|
+
* The native Rust engine has no knowledge of JS-layer fnRefBindings (e.g.
|
|
434
|
+
* `const f = fn.bind(ctx)`), so calls to bind-created aliases are not resolved
|
|
435
|
+
* to their original function on the native path. This JS post-pass runs after
|
|
436
|
+
* the native edge pass and adds only the fnRefBindings-seeded pts edges that the
|
|
437
|
+
* native engine missed.
|
|
438
|
+
*
|
|
439
|
+
* Uses the same seenByPair dedup guard as buildParamFlowPtsPostPass to avoid
|
|
440
|
+
* duplicating edges already emitted by the native engine.
|
|
441
|
+
*/
|
|
442
|
+
function buildFnRefBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup) {
|
|
443
|
+
// Only process files that actually have fnRefBindings.
|
|
444
|
+
const filesWithBindings = [...ctx.fileSymbols].filter(([, symbols]) => symbols.fnRefBindings && symbols.fnRefBindings.length > 0);
|
|
445
|
+
if (filesWithBindings.length === 0)
|
|
446
|
+
return;
|
|
447
|
+
// Seed seenByPair from the existing rows so we don't duplicate native edges.
|
|
448
|
+
const seenByPair = new Set();
|
|
449
|
+
for (const [srcId, tgtId] of allEdgeRows) {
|
|
450
|
+
seenByPair.add(`${srcId}|${tgtId}`);
|
|
451
|
+
}
|
|
452
|
+
const { barrelOnlyFiles, rootDir } = ctx;
|
|
453
|
+
const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
|
|
454
|
+
for (const [relPath, symbols] of filesWithBindings) {
|
|
455
|
+
if (barrelOnlyFiles.has(relPath))
|
|
456
|
+
continue;
|
|
457
|
+
const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
458
|
+
if (!fileNodeRow)
|
|
459
|
+
continue;
|
|
460
|
+
const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
|
|
461
|
+
const typeMap = symbols.typeMap || new Map();
|
|
462
|
+
const ptsMap = buildPointsToMapForFile(symbols, importedNames);
|
|
463
|
+
if (!ptsMap)
|
|
464
|
+
continue;
|
|
465
|
+
// Only resolve calls whose name is an lhs in fnRefBindings — the same
|
|
466
|
+
// narrowed guard used in buildFileCallEdges case (c).
|
|
467
|
+
const fnRefBindingLhs = new Set(symbols.fnRefBindings.map((b) => b.lhs));
|
|
468
|
+
for (const call of symbols.calls) {
|
|
469
|
+
if (call.receiver || call.dynamic)
|
|
470
|
+
continue; // bind aliases are flat-keyed, never dynamic
|
|
471
|
+
if (!fnRefBindingLhs.has(call.name))
|
|
472
|
+
continue;
|
|
473
|
+
if (!ptsMap.has(call.name))
|
|
474
|
+
continue;
|
|
475
|
+
const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
|
|
476
|
+
// Only resolve calls that had no direct targets (same guard as buildFileCallEdges).
|
|
477
|
+
const { targets } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap);
|
|
478
|
+
if (targets.length > 0)
|
|
479
|
+
continue;
|
|
480
|
+
for (const alias of resolveViaPointsTo(call.name, ptsMap)) {
|
|
481
|
+
const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
|
|
482
|
+
for (const t of aliasTargets) {
|
|
483
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
484
|
+
if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
|
|
485
|
+
const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
|
|
486
|
+
if (conf > 0) {
|
|
487
|
+
seenByPair.add(edgeKey);
|
|
488
|
+
allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* this-rebinding post-pass for the native call-edge path.
|
|
498
|
+
*
|
|
499
|
+
* When `fn.call(namedCtx, ...)` or `fn.apply(namedCtx, ...)` is extracted by the
|
|
500
|
+
* WASM layer, `thisCallBindings` records `{ callee: 'fn', thisArg: 'namedCtx' }`.
|
|
501
|
+
* The native Rust engine has no knowledge of these bindings, so `this()` calls
|
|
502
|
+
* inside `fn` remain unresolved. This JS post-pass adds the missing edges by
|
|
503
|
+
* resolving `this()` calls inside each `fn` that has a thisCallBinding.
|
|
504
|
+
*/
|
|
505
|
+
function buildThisCallBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup) {
|
|
506
|
+
const filesWithBindings = [...ctx.fileSymbols].filter(([, symbols]) => symbols.thisCallBindings && symbols.thisCallBindings.length > 0);
|
|
507
|
+
if (filesWithBindings.length === 0)
|
|
508
|
+
return;
|
|
509
|
+
const seenByPair = new Set();
|
|
510
|
+
for (const [srcId, tgtId] of allEdgeRows) {
|
|
511
|
+
seenByPair.add(`${srcId}|${tgtId}`);
|
|
512
|
+
}
|
|
513
|
+
const { barrelOnlyFiles, rootDir } = ctx;
|
|
514
|
+
const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
|
|
515
|
+
for (const [relPath, symbols] of filesWithBindings) {
|
|
516
|
+
if (barrelOnlyFiles.has(relPath))
|
|
517
|
+
continue;
|
|
518
|
+
const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
519
|
+
if (!fileNodeRow)
|
|
520
|
+
continue;
|
|
521
|
+
const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
|
|
522
|
+
const typeMap = symbols.typeMap || new Map();
|
|
523
|
+
const ptsMap = buildPointsToMapForFile(symbols, importedNames);
|
|
524
|
+
if (!ptsMap)
|
|
525
|
+
continue;
|
|
526
|
+
// Only process calls named 'this' (callee-not-receiver usage)
|
|
527
|
+
for (const call of symbols.calls) {
|
|
528
|
+
if (call.name !== 'this' || call.receiver)
|
|
529
|
+
continue;
|
|
530
|
+
const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
|
|
531
|
+
if (caller.callerName == null)
|
|
532
|
+
continue;
|
|
533
|
+
const scopedKey = `${caller.callerName}::this`;
|
|
534
|
+
if (!ptsMap.has(scopedKey))
|
|
535
|
+
continue;
|
|
536
|
+
for (const alias of resolveViaPointsTo(scopedKey, ptsMap)) {
|
|
537
|
+
const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
|
|
538
|
+
for (const t of aliasTargets) {
|
|
539
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
540
|
+
if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
|
|
541
|
+
const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
|
|
542
|
+
if (conf > 0) {
|
|
543
|
+
seenByPair.add(edgeKey);
|
|
544
|
+
allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Phase 8.3f post-pass for the native call-edge path.
|
|
554
|
+
*
|
|
555
|
+
* The native Rust engine builds call edges without knowledge of
|
|
556
|
+
* objectRestParamBindings, so `rest.method()` calls inside functions with
|
|
557
|
+
* object-destructuring rest parameters are not resolved via the typeMap chain.
|
|
558
|
+
* The Rust engine already resolves same-file and directly-imported callees
|
|
559
|
+
* (via steps 1–2 of its resolution logic), so this post-pass only adds edges
|
|
560
|
+
* that require the typeMap-chain path:
|
|
561
|
+
* typeMap[restName] → argName → typeMap[argName.method] → target
|
|
562
|
+
*
|
|
563
|
+
* Mirrors the seeding in buildCallEdgesJS (Phase 8.3f) to ensure both engine
|
|
564
|
+
* paths produce identical results for receiver-typed rest-param calls.
|
|
565
|
+
*/
|
|
566
|
+
function buildObjectRestParamPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup) {
|
|
567
|
+
const filesWithRestBindings = [...ctx.fileSymbols].filter(([, symbols]) => symbols.objectRestParamBindings &&
|
|
568
|
+
symbols.objectRestParamBindings.length > 0 &&
|
|
569
|
+
symbols.paramBindings &&
|
|
570
|
+
symbols.paramBindings.length > 0);
|
|
571
|
+
if (filesWithRestBindings.length === 0)
|
|
572
|
+
return;
|
|
573
|
+
const seenByPair = new Set();
|
|
574
|
+
for (const [srcId, tgtId] of allEdgeRows) {
|
|
575
|
+
seenByPair.add(`${srcId}|${tgtId}`);
|
|
576
|
+
}
|
|
577
|
+
const { barrelOnlyFiles, rootDir } = ctx;
|
|
578
|
+
const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
|
|
579
|
+
for (const [relPath, symbols] of filesWithRestBindings) {
|
|
580
|
+
if (barrelOnlyFiles.has(relPath))
|
|
581
|
+
continue;
|
|
582
|
+
const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
583
|
+
if (!fileNodeRow)
|
|
584
|
+
continue;
|
|
585
|
+
const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
|
|
586
|
+
const typeMap = new Map(symbols.typeMap instanceof Map ? symbols.typeMap : []);
|
|
587
|
+
// Seed typeMap[callee::restName] = { type: argName } for each matching pair.
|
|
588
|
+
// Mirrors the seeding in buildCallEdgesJS Phase 8.3f. Keys are scoped by
|
|
589
|
+
// callee so two functions with the same rest-param name (e.g. `...rest`) in
|
|
590
|
+
// the same file don't collide (#1358).
|
|
591
|
+
// When only one callee uses a given rest name, also seed the unscoped key
|
|
592
|
+
// as a null-callerName fallback so edges aren't silently dropped if
|
|
593
|
+
// findCaller can't identify the enclosing function (#1358).
|
|
594
|
+
const restNameCallees = new Map();
|
|
595
|
+
for (const orpb of symbols.objectRestParamBindings) {
|
|
596
|
+
if (!restNameCallees.has(orpb.restName))
|
|
597
|
+
restNameCallees.set(orpb.restName, new Set());
|
|
598
|
+
restNameCallees.get(orpb.restName).add(orpb.callee);
|
|
599
|
+
}
|
|
600
|
+
const restNames = new Set();
|
|
601
|
+
for (const orpb of symbols.objectRestParamBindings) {
|
|
602
|
+
for (const pb of symbols.paramBindings) {
|
|
603
|
+
if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) {
|
|
604
|
+
const scopedKey = `${orpb.callee}::${orpb.restName}`;
|
|
605
|
+
if (!typeMap.has(scopedKey)) {
|
|
606
|
+
typeMap.set(scopedKey, { type: pb.argName, confidence: 0.65 });
|
|
607
|
+
if (restNameCallees.get(orpb.restName).size === 1 && !typeMap.has(orpb.restName)) {
|
|
608
|
+
typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// restNames tracks every rest-parameter name found, regardless of whether the
|
|
612
|
+
// scoped key was already in typeMap. This ensures the post-pass (below) processes
|
|
613
|
+
// all calls whose receiver matches a known rest binding — not just those whose
|
|
614
|
+
// typeMap entry was seeded in this iteration.
|
|
615
|
+
restNames.add(orpb.restName);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (restNames.size === 0)
|
|
620
|
+
continue;
|
|
621
|
+
for (const call of symbols.calls) {
|
|
622
|
+
// Only process calls whose receiver is a known rest-binding name.
|
|
623
|
+
if (!call.receiver || !restNames.has(call.receiver))
|
|
624
|
+
continue;
|
|
625
|
+
const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
|
|
626
|
+
// Resolve with the enriched typeMap. callerName is passed so
|
|
627
|
+
// resolveByMethodOrGlobal can look up the scoped key callee::restName (#1358).
|
|
628
|
+
// seenByPair deduplicates edges the native engine already emitted.
|
|
629
|
+
const { targets, importedFrom } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap, caller.callerName);
|
|
630
|
+
for (const t of targets) {
|
|
631
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
632
|
+
if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
|
|
633
|
+
const conf = computeConfidence(relPath, t.file, importedFrom ?? null) - PROPAGATION_HOP_PENALTY;
|
|
634
|
+
if (conf > 0) {
|
|
635
|
+
seenByPair.add(edgeKey);
|
|
636
|
+
allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'points-to']);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Object.defineProperty accessor post-pass for the native call-edge path.
|
|
645
|
+
*
|
|
646
|
+
* When a function is registered as a getter/setter via
|
|
647
|
+
* `Object.defineProperty(obj, "bar", { get: getter })`, calls to `this.X()`
|
|
648
|
+
* inside `getter` need to resolve against `obj` (because `this === obj` when
|
|
649
|
+
* the accessor is invoked). The native Rust engine has no knowledge of
|
|
650
|
+
* `definePropertyReceivers`, so this JS post-pass adds the missing edges.
|
|
651
|
+
*/
|
|
652
|
+
function buildDefinePropertyPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup) {
|
|
653
|
+
const filesWithReceivers = [...ctx.fileSymbols].filter(([, symbols]) => symbols.definePropertyReceivers && symbols.definePropertyReceivers.size > 0);
|
|
654
|
+
if (filesWithReceivers.length === 0)
|
|
655
|
+
return;
|
|
656
|
+
const seenByPair = new Set();
|
|
657
|
+
for (const [srcId, tgtId] of allEdgeRows) {
|
|
658
|
+
seenByPair.add(`${srcId}|${tgtId}`);
|
|
659
|
+
}
|
|
660
|
+
const { barrelOnlyFiles, rootDir } = ctx;
|
|
661
|
+
const lookup = sharedLookup ?? makeContextLookup(ctx, getNodeIdStmt);
|
|
662
|
+
for (const [relPath, symbols] of filesWithReceivers) {
|
|
663
|
+
if (barrelOnlyFiles.has(relPath))
|
|
664
|
+
continue;
|
|
665
|
+
const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
666
|
+
if (!fileNodeRow)
|
|
667
|
+
continue;
|
|
668
|
+
const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
|
|
669
|
+
const typeMap = symbols.typeMap || new Map();
|
|
670
|
+
const definePropertyReceivers = symbols.definePropertyReceivers;
|
|
671
|
+
for (const call of symbols.calls) {
|
|
672
|
+
if (call.receiver !== 'this')
|
|
673
|
+
continue;
|
|
674
|
+
const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
|
|
675
|
+
if (!caller.callerName)
|
|
676
|
+
continue;
|
|
677
|
+
const receiverVarName = definePropertyReceivers.get(caller.callerName);
|
|
678
|
+
if (!receiverVarName)
|
|
679
|
+
continue;
|
|
680
|
+
// Only add edges the native engine missed (no direct target already).
|
|
681
|
+
const { targets: directTargets } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap, caller.callerName);
|
|
682
|
+
if (directTargets.length > 0)
|
|
683
|
+
continue;
|
|
684
|
+
// Resolve via receiver type
|
|
685
|
+
let targets = [];
|
|
686
|
+
const typeEntry = typeMap.get(receiverVarName);
|
|
687
|
+
const typeName = typeEntry
|
|
688
|
+
? typeof typeEntry === 'string'
|
|
689
|
+
? typeEntry
|
|
690
|
+
: typeEntry.type
|
|
691
|
+
: null;
|
|
692
|
+
if (typeName) {
|
|
693
|
+
const qualifiedName = `${typeName}.${call.name}`;
|
|
694
|
+
targets = lookup.byNameAndFile(qualifiedName, relPath);
|
|
695
|
+
}
|
|
696
|
+
// Same-file fallback for plain object-literal methods
|
|
697
|
+
if (targets.length === 0) {
|
|
698
|
+
targets = lookup.byNameAndFile(call.name, relPath);
|
|
699
|
+
}
|
|
700
|
+
for (const t of targets) {
|
|
701
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
702
|
+
if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
|
|
703
|
+
const conf = computeConfidence(relPath, t.file, null);
|
|
704
|
+
if (conf > 0) {
|
|
705
|
+
seenByPair.add(edgeKey);
|
|
706
|
+
allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'ts-native']);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Phase 8.5: CHA + RTA post-pass for the native call-edge path.
|
|
715
|
+
*
|
|
716
|
+
* The native Rust engine has no knowledge of the CHA context, so `this.method()`
|
|
717
|
+
* calls and interface method dispatches are not expanded to their concrete
|
|
718
|
+
* implementations. This JS post-pass runs after the native edges (and the pts
|
|
719
|
+
* post-pass) and adds only the CHA-resolved edges that the native engine missed.
|
|
720
|
+
*
|
|
721
|
+
* Like buildParamFlowPtsPostPass, it seeds seenByPair from the current allEdgeRows
|
|
722
|
+
* snapshot to avoid duplicating edges the native engine already produced.
|
|
723
|
+
*/
|
|
724
|
+
function buildChaPostPass(ctx, getNodeIdStmt, allEdgeRows, chaCtx) {
|
|
725
|
+
// Fast-exit when the CHA context is empty (no class hierarchy in the project)
|
|
726
|
+
if (chaCtx.implementors.size === 0 && chaCtx.parents.size === 0)
|
|
727
|
+
return;
|
|
728
|
+
// Seed only from 'calls' edges — import/extends/implements edges share (src,tgt) pairs
|
|
729
|
+
// with real call edges at the file-node level and would cause false dedup if included.
|
|
730
|
+
const seenByPair = new Set();
|
|
731
|
+
for (const row of allEdgeRows) {
|
|
732
|
+
if (row[2] === 'calls')
|
|
733
|
+
seenByPair.add(`${row[0]}|${row[1]}`);
|
|
734
|
+
}
|
|
735
|
+
const { fileSymbols, barrelOnlyFiles } = ctx;
|
|
736
|
+
const lookup = makeContextLookup(ctx, getNodeIdStmt);
|
|
737
|
+
for (const [relPath, symbols] of fileSymbols) {
|
|
738
|
+
if (barrelOnlyFiles.has(relPath))
|
|
739
|
+
continue;
|
|
740
|
+
const fileNodeRow = getNodeIdStmt.get(relPath, 'file', relPath, 0);
|
|
741
|
+
if (!fileNodeRow)
|
|
742
|
+
continue;
|
|
743
|
+
const typeMap = symbols.typeMap || new Map();
|
|
744
|
+
for (const call of symbols.calls) {
|
|
745
|
+
if (!call.receiver)
|
|
746
|
+
continue;
|
|
747
|
+
if (BUILTIN_RECEIVERS.has(call.receiver))
|
|
748
|
+
continue;
|
|
749
|
+
const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
|
|
750
|
+
let chaTargets = [];
|
|
751
|
+
if (call.receiver === 'this' || call.receiver === 'self' || call.receiver === 'super') {
|
|
752
|
+
chaTargets = resolveThisDispatch(call.name, caller.callerName, call.receiver, chaCtx, lookup);
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
const typeEntry = typeMap.get(call.receiver);
|
|
756
|
+
const typeName = typeEntry
|
|
757
|
+
? typeof typeEntry === 'string'
|
|
758
|
+
? typeEntry
|
|
759
|
+
: typeEntry.type
|
|
760
|
+
: null;
|
|
761
|
+
if (typeName) {
|
|
762
|
+
chaTargets = resolveChaTargets(typeName, call.name, chaCtx, lookup);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
for (const t of chaTargets) {
|
|
766
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
767
|
+
if (t.id !== caller.id && !seenByPair.has(edgeKey)) {
|
|
768
|
+
const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
|
|
769
|
+
if (conf > 0) {
|
|
770
|
+
seenByPair.add(edgeKey);
|
|
771
|
+
allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'cha']);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
291
776
|
}
|
|
292
777
|
}
|
|
293
778
|
function buildImportedNamesForNative(ctx, relPath, symbols, rootDir) {
|
|
@@ -301,7 +786,7 @@ function buildImportedNamesForNative(ctx, relPath, symbols, rootDir) {
|
|
|
301
786
|
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
302
787
|
let targetFile = resolvedPath;
|
|
303
788
|
if (isBarrelFile(ctx, resolvedPath)) {
|
|
304
|
-
const actual =
|
|
789
|
+
const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
|
|
305
790
|
if (actual)
|
|
306
791
|
targetFile = actual;
|
|
307
792
|
}
|
|
@@ -319,7 +804,7 @@ function buildImportedNamesForNative(ctx, relPath, symbols, rootDir) {
|
|
|
319
804
|
return importedNames;
|
|
320
805
|
}
|
|
321
806
|
// ── Call edges (JS fallback) ────────────────────────────────────────────
|
|
322
|
-
function buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows) {
|
|
807
|
+
function buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows, chaCtx) {
|
|
323
808
|
const { fileSymbols, barrelOnlyFiles, rootDir } = ctx;
|
|
324
809
|
const lookup = makeContextLookup(ctx, getNodeIdStmt);
|
|
325
810
|
for (const [relPath, symbols] of fileSymbols) {
|
|
@@ -329,9 +814,36 @@ function buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows) {
|
|
|
329
814
|
if (!fileNodeRow)
|
|
330
815
|
continue;
|
|
331
816
|
const importedNames = buildImportedNamesMap(ctx, relPath, symbols, rootDir);
|
|
332
|
-
const typeMap = symbols.typeMap
|
|
817
|
+
const typeMap = new Map(symbols.typeMap instanceof Map ? symbols.typeMap : []);
|
|
818
|
+
// Phase 8.3f: seed typeMap[callee::restName] = { type: argName } for each
|
|
819
|
+
// object-destructuring rest parameter binding × call-site argument binding.
|
|
820
|
+
// Keys are scoped so two functions with the same rest-param name in the same
|
|
821
|
+
// file don't collide (#1358). When only one callee uses a given rest name,
|
|
822
|
+
// also seed the unscoped key as a null-callerName fallback.
|
|
823
|
+
if (symbols.objectRestParamBindings?.length && symbols.paramBindings?.length) {
|
|
824
|
+
const restNameCallees = new Map();
|
|
825
|
+
for (const orpb of symbols.objectRestParamBindings) {
|
|
826
|
+
if (!restNameCallees.has(orpb.restName))
|
|
827
|
+
restNameCallees.set(orpb.restName, new Set());
|
|
828
|
+
restNameCallees.get(orpb.restName).add(orpb.callee);
|
|
829
|
+
}
|
|
830
|
+
for (const orpb of symbols.objectRestParamBindings) {
|
|
831
|
+
for (const pb of symbols.paramBindings) {
|
|
832
|
+
if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) {
|
|
833
|
+
const scopedKey = `${orpb.callee}::${orpb.restName}`;
|
|
834
|
+
if (!typeMap.has(scopedKey)) {
|
|
835
|
+
typeMap.set(scopedKey, { type: pb.argName, confidence: 0.65 });
|
|
836
|
+
if (restNameCallees.get(orpb.restName).size === 1 && !typeMap.has(orpb.restName)) {
|
|
837
|
+
typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 });
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
333
844
|
const seenCallEdges = new Set();
|
|
334
|
-
|
|
845
|
+
const ptsMap = buildPointsToMapForFile(symbols, importedNames);
|
|
846
|
+
buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCallEdges, lookup, allEdgeRows, typeMap, ptsMap, chaCtx);
|
|
335
847
|
buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows);
|
|
336
848
|
}
|
|
337
849
|
}
|
|
@@ -341,12 +853,23 @@ function buildImportedNamesMap(ctx, relPath, symbols, rootDir) {
|
|
|
341
853
|
// (higher priority). Static imports represent direct bindings while dynamic
|
|
342
854
|
// imports often use aliased destructuring (`{ foo: bar } = await import(…)`).
|
|
343
855
|
// When both contribute the same name, the static binding is authoritative.
|
|
856
|
+
//
|
|
857
|
+
// Phase 8.4: trace through barrel files so that symbol names map to their
|
|
858
|
+
// actual definition file, not the re-exporting barrel. Mirrors the tracing
|
|
859
|
+
// already done in buildImportedNamesForNative (the native path).
|
|
860
|
+
const traceBarrel = (resolvedPath, cleanName) => {
|
|
861
|
+
if (!isBarrelFile(ctx, resolvedPath))
|
|
862
|
+
return resolvedPath;
|
|
863
|
+
const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
|
|
864
|
+
return actual ?? resolvedPath;
|
|
865
|
+
};
|
|
344
866
|
for (const imp of symbols.imports) {
|
|
345
867
|
if (!imp.dynamicImport)
|
|
346
868
|
continue;
|
|
347
869
|
const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
|
|
348
870
|
for (const name of imp.names) {
|
|
349
|
-
|
|
871
|
+
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
872
|
+
importedNames.set(cleanName, traceBarrel(resolvedPath, cleanName));
|
|
350
873
|
}
|
|
351
874
|
}
|
|
352
875
|
for (const imp of symbols.imports) {
|
|
@@ -355,7 +878,7 @@ function buildImportedNamesMap(ctx, relPath, symbols, rootDir) {
|
|
|
355
878
|
const resolvedPath = getResolved(ctx, path.join(rootDir, relPath), imp.source);
|
|
356
879
|
for (const name of imp.names) {
|
|
357
880
|
const cleanName = name.replace(/^\*\s+as\s+/, '');
|
|
358
|
-
importedNames.set(cleanName, resolvedPath);
|
|
881
|
+
importedNames.set(cleanName, traceBarrel(resolvedPath, cleanName));
|
|
359
882
|
}
|
|
360
883
|
}
|
|
361
884
|
return importedNames;
|
|
@@ -365,23 +888,281 @@ function makeContextLookup(ctx, getNodeIdStmt) {
|
|
|
365
888
|
byNameAndFile: (name, file) => ctx.nodesByNameAndFile.get(`${name}|${file}`) ?? [],
|
|
366
889
|
byName: (name) => ctx.nodesByName.get(name) ?? [],
|
|
367
890
|
isBarrel: (file) => isBarrelFile(ctx, file),
|
|
368
|
-
resolveBarrel: (barrelFile, symbolName) =>
|
|
891
|
+
resolveBarrel: (barrelFile, symbolName) => resolveBarrelExportCached(ctx, barrelFile, symbolName),
|
|
369
892
|
nodeId: (name, kind, file, line) => getNodeIdStmt.get(name, kind, file, line),
|
|
370
893
|
};
|
|
371
894
|
}
|
|
372
|
-
|
|
895
|
+
/**
|
|
896
|
+
* Build a per-file points-to map for Phase 8.3 alias resolution.
|
|
897
|
+
* Returns null fast when the file has no function-reference bindings.
|
|
898
|
+
*
|
|
899
|
+
* Only callable definitions (function/method) are seeded as concrete targets.
|
|
900
|
+
* Class and interface names are intentionally excluded — aliasing a constructor
|
|
901
|
+
* (`const Svc = MyService`) is an uncommon pattern that would require tracking
|
|
902
|
+
* `new`-expression flows separately from the alias chain. That is left to Phase
|
|
903
|
+
* 8.2 call-assignment propagation, which already handles constructor assignments.
|
|
904
|
+
*/
|
|
905
|
+
function buildPointsToMapForFile(symbols, importedNames) {
|
|
906
|
+
const hasThisCallBindings = !!symbols.thisCallBindings?.length;
|
|
907
|
+
if (!symbols.fnRefBindings?.length &&
|
|
908
|
+
!symbols.paramBindings?.length &&
|
|
909
|
+
!symbols.arrayElemBindings?.length &&
|
|
910
|
+
!symbols.spreadArgBindings?.length &&
|
|
911
|
+
!symbols.forOfBindings?.length &&
|
|
912
|
+
!symbols.arrayCallbackBindings?.length &&
|
|
913
|
+
!symbols.objectRestParamBindings?.length &&
|
|
914
|
+
!symbols.objectPropBindings?.length &&
|
|
915
|
+
!hasThisCallBindings)
|
|
916
|
+
return null;
|
|
917
|
+
const defNames = new Set(symbols.definitions
|
|
918
|
+
.filter((d) => d.kind === 'function' || d.kind === 'method')
|
|
919
|
+
.map((d) => d.name));
|
|
920
|
+
const definitionParams = buildDefinitionParamsMap(symbols.definitions);
|
|
921
|
+
// Convert thisCallBindings into scoped fnRefBindings: `fn::this → namedCtx`.
|
|
922
|
+
// The scoped key `fn::this` is looked up when `this()` calls are resolved inside
|
|
923
|
+
// function `fn` — caller.callerName='fn', call.name='this' → scopedPtsKey='fn::this'.
|
|
924
|
+
let allFnRefBindings = symbols.fnRefBindings ?? [];
|
|
925
|
+
if (hasThisCallBindings) {
|
|
926
|
+
const extra = (symbols.thisCallBindings ?? []).map((b) => ({
|
|
927
|
+
lhs: `${b.callee}::this`,
|
|
928
|
+
rhs: b.thisArg,
|
|
929
|
+
}));
|
|
930
|
+
allFnRefBindings = [...allFnRefBindings, ...extra];
|
|
931
|
+
}
|
|
932
|
+
return buildPointsToMap(allFnRefBindings, defNames, importedNames, symbols.paramBindings, definitionParams, symbols.arrayElemBindings, symbols.spreadArgBindings, symbols.forOfBindings, symbols.arrayCallbackBindings, symbols.objectRestParamBindings, symbols.objectPropBindings);
|
|
933
|
+
}
|
|
934
|
+
function buildDefinitionParamsMap(definitions) {
|
|
935
|
+
const map = new Map();
|
|
936
|
+
for (const def of definitions) {
|
|
937
|
+
if ((def.kind === 'function' || def.kind === 'method') && def.children) {
|
|
938
|
+
const params = def.children.filter((c) => c.kind === 'parameter').map((c) => c.name);
|
|
939
|
+
if (params.length > 0) {
|
|
940
|
+
if (map.has(def.name)) {
|
|
941
|
+
// Two definitions share the same name (e.g. overloads, same-named method and
|
|
942
|
+
// function, or conditional redeclaration). Keep the first entry — using the
|
|
943
|
+
// wrong parameter list would map argIndex to the wrong parameter name.
|
|
944
|
+
debug(`buildDefinitionParamsMap: duplicate def name "${def.name}" (kind=${def.kind}, line=${def.line}) — skipping; first entry kept`);
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
map.set(def.name, params);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return map;
|
|
953
|
+
}
|
|
954
|
+
function buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCallEdges, lookup, allEdgeRows, typeMap, ptsMap, chaCtx) {
|
|
955
|
+
// Tracks edges that were inserted by the pts fallback (edgeKey → allEdgeRows index).
|
|
956
|
+
// Kept separate from seenCallEdges so that a subsequent direct-call edge for the same
|
|
957
|
+
// caller→target pair can upgrade the confidence in-place rather than being silently
|
|
958
|
+
// dropped by the dedup guard. Once upgraded, the key moves to seenCallEdges and is
|
|
959
|
+
// no longer tracked here.
|
|
960
|
+
const ptsEdgeRows = new Map();
|
|
961
|
+
// Pre-compute the set of names that appear as lhs in fnRefBindings so that
|
|
962
|
+
// case (c) of the pts gate below only fires for names that are genuine
|
|
963
|
+
// bind/alias entries, not for every locally-defined function or import that
|
|
964
|
+
// buildPointsToMap seeds with a self-pointing entry.
|
|
965
|
+
const fnRefBindingLhs = new Set(symbols.fnRefBindings?.map((b) => b.lhs) ?? []);
|
|
373
966
|
for (const call of symbols.calls) {
|
|
374
967
|
if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver))
|
|
375
968
|
continue;
|
|
376
969
|
const caller = findCaller(lookup, call, symbols.definitions, relPath, fileNodeRow);
|
|
377
970
|
const isDynamic = call.dynamic ? 1 : 0;
|
|
378
|
-
|
|
971
|
+
let { targets, importedFrom } = resolveCallTargets(lookup, call, relPath, importedNames, typeMap, caller.callerName);
|
|
972
|
+
// Same-class `this.method()` fallback: when the call receiver is `this` and
|
|
973
|
+
// resolveCallTargets found nothing, derive the enclosing class name from the
|
|
974
|
+
// caller (e.g. `Logger.info` → class prefix `Logger`) and retry with the
|
|
975
|
+
// qualified method name `Logger._write`. This mirrors what the native Rust
|
|
976
|
+
// engine does implicitly via its class-scoped symbol table.
|
|
977
|
+
// NOTE: restricted to `this` only — `super.method()` targets a parent class,
|
|
978
|
+
// not the enclosing class, so qualifying with the child class name would
|
|
979
|
+
// produce a false edge when the child also defines a same-named method.
|
|
980
|
+
if (targets.length === 0 && call.receiver === 'this' && caller.callerName != null) {
|
|
981
|
+
const lastDot = caller.callerName.lastIndexOf('.');
|
|
982
|
+
if (lastDot > 0) {
|
|
983
|
+
const prevDot = caller.callerName.lastIndexOf('.', lastDot - 1);
|
|
984
|
+
const className = caller.callerName.slice(prevDot + 1, lastDot);
|
|
985
|
+
const qualifiedName = `${className}.${call.name}`;
|
|
986
|
+
const qualified = lookup
|
|
987
|
+
.byNameAndFile(qualifiedName, relPath)
|
|
988
|
+
.filter((n) => n.kind === 'method');
|
|
989
|
+
if (qualified.length > 0) {
|
|
990
|
+
targets = qualified;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
// Same-class bare-call fallback: when a no-receiver call can't be resolved
|
|
995
|
+
// globally, try the caller's own class as a qualifier. Handles C# static
|
|
996
|
+
// sibling calls: `IsValidEmail()` inside `Validators.ValidateUser` resolves
|
|
997
|
+
// to `Validators.IsValidEmail`. Skipped for JS/TS where bare calls are
|
|
998
|
+
// module-scoped, not class-scoped.
|
|
999
|
+
if (targets.length === 0 &&
|
|
1000
|
+
!call.receiver &&
|
|
1001
|
+
caller.callerName != null &&
|
|
1002
|
+
!isModuleScopedLanguage(relPath)) {
|
|
1003
|
+
const lastDot = caller.callerName.lastIndexOf('.');
|
|
1004
|
+
if (lastDot > 0) {
|
|
1005
|
+
const prevDot = caller.callerName.lastIndexOf('.', lastDot - 1);
|
|
1006
|
+
const className = caller.callerName.slice(prevDot + 1, lastDot);
|
|
1007
|
+
const qualifiedName = `${className}.${call.name}`;
|
|
1008
|
+
const qualified = lookup
|
|
1009
|
+
.byNameAndFile(qualifiedName, relPath)
|
|
1010
|
+
.filter((n) => n.kind === 'method');
|
|
1011
|
+
if (qualified.length > 0) {
|
|
1012
|
+
targets = qualified;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
// Object.defineProperty accessor fallback: when a function is registered as
|
|
1017
|
+
// a getter/setter via `Object.defineProperty(obj, "bar", { get: getter })`,
|
|
1018
|
+
// calls to `this.X()` inside `getter` resolve against `obj` (this === obj
|
|
1019
|
+
// when the accessor is invoked). If the same-class fallback above found
|
|
1020
|
+
// nothing, try treating `obj` as the receiver and look up `obj.X` in the
|
|
1021
|
+
// typeMap, or fall back to a same-file lookup of any definition named X
|
|
1022
|
+
// that belongs to the object literal or its type.
|
|
1023
|
+
if (targets.length === 0 &&
|
|
1024
|
+
call.receiver === 'this' &&
|
|
1025
|
+
caller.callerName != null &&
|
|
1026
|
+
symbols.definePropertyReceivers) {
|
|
1027
|
+
const receiverVarName = symbols.definePropertyReceivers.get(caller.callerName);
|
|
1028
|
+
if (receiverVarName) {
|
|
1029
|
+
// Try typeMap lookup for receiver.methodName
|
|
1030
|
+
const typeEntry = typeMap.get(receiverVarName);
|
|
1031
|
+
const typeName = typeEntry
|
|
1032
|
+
? typeof typeEntry === 'string'
|
|
1033
|
+
? typeEntry
|
|
1034
|
+
: typeEntry.type
|
|
1035
|
+
: null;
|
|
1036
|
+
if (typeName) {
|
|
1037
|
+
const qualifiedName = `${typeName}.${call.name}`;
|
|
1038
|
+
const qualified = lookup.byNameAndFile(qualifiedName, relPath);
|
|
1039
|
+
if (qualified.length > 0) {
|
|
1040
|
+
targets = [...qualified];
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
// If still no targets, search for any definition named `call.name` in
|
|
1044
|
+
// the same file — handles plain object literals where the method isn't
|
|
1045
|
+
// qualified (e.g. `const obj = { baz() {} }` defines `baz` directly).
|
|
1046
|
+
// Note: this is intentionally broad — it matches any same-file definition
|
|
1047
|
+
// with the called name, not just members of the receiver object. This is
|
|
1048
|
+
// the same behaviour used by the native post-pass path (buildDefinePropertyPostPass).
|
|
1049
|
+
if (targets.length === 0) {
|
|
1050
|
+
const sameFile = lookup.byNameAndFile(call.name, relPath);
|
|
1051
|
+
if (sameFile.length > 0) {
|
|
1052
|
+
targets = [...sameFile];
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
379
1057
|
for (const t of targets) {
|
|
380
1058
|
const edgeKey = `${caller.id}|${t.id}`;
|
|
381
|
-
if (t.id !== caller.id
|
|
382
|
-
seenCallEdges.add(edgeKey);
|
|
1059
|
+
if (t.id !== caller.id) {
|
|
383
1060
|
const confidence = computeConfidence(relPath, t.file, importedFrom ?? null);
|
|
384
|
-
|
|
1061
|
+
if (seenCallEdges.has(edgeKey))
|
|
1062
|
+
continue;
|
|
1063
|
+
const ptsIdx = ptsEdgeRows.get(edgeKey);
|
|
1064
|
+
if (ptsIdx !== undefined) {
|
|
1065
|
+
// A pts-resolved edge already exists for this caller→target pair with a
|
|
1066
|
+
// penalised confidence. Upgrade it to the direct-call confidence in-place,
|
|
1067
|
+
// then promote to seenCallEdges so no further processing is needed.
|
|
1068
|
+
const ptsRow = allEdgeRows[ptsIdx];
|
|
1069
|
+
if (ptsRow) {
|
|
1070
|
+
ptsRow[3] = confidence;
|
|
1071
|
+
ptsRow[4] = isDynamic; // upgrade is_dynamic: direct call overrides the pts-alias dynamic flag
|
|
1072
|
+
ptsRow[5] = 'ts-native'; // promoted from pts to direct-call resolution
|
|
1073
|
+
}
|
|
1074
|
+
ptsEdgeRows.delete(edgeKey);
|
|
1075
|
+
seenCallEdges.add(edgeKey);
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
seenCallEdges.add(edgeKey);
|
|
1079
|
+
allEdgeRows.push([caller.id, t.id, 'calls', confidence, isDynamic, 'ts-native']);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
// Phase 8.3 / 8.3c / bind: points-to fallback for unresolved calls.
|
|
1084
|
+
// Fires for three cases:
|
|
1085
|
+
// (a) dynamic=true: alias calls emitted by extractCallbackReferenceCalls.
|
|
1086
|
+
// Looks up `call.name` directly (alias entries are flat-keyed).
|
|
1087
|
+
// (b) non-dynamic: parameter variable calls (fn() where fn is a param).
|
|
1088
|
+
// Looks up the scoped key `callerName::call.name` to avoid spurious
|
|
1089
|
+
// edges from same-named parameters across different functions.
|
|
1090
|
+
// (c) non-dynamic: module-level alias bindings — `f = fn.bind(ctx)` or
|
|
1091
|
+
// `const f = handler` — where pts('f') was seeded by fnRefBindings.
|
|
1092
|
+
// Checked against fnRefBindingLhs (the pre-computed set of lhs names from
|
|
1093
|
+
// fnRefBindings) rather than the full ptsMap, so case (c) only fires for
|
|
1094
|
+
// genuine bind/alias entries and never for self-seeded local definitions.
|
|
1095
|
+
// Confidence is penalised by one hop to reflect the extra indirection.
|
|
1096
|
+
//
|
|
1097
|
+
// Note: pts edges are added to ptsEdgeRows (not seenCallEdges) so that a later
|
|
1098
|
+
// direct call to the same target in the same function body can upgrade confidence
|
|
1099
|
+
// rather than being silently dropped by the dedup guard.
|
|
1100
|
+
const scopedPtsKey = caller.callerName != null ? `${caller.callerName}::${call.name}` : null;
|
|
1101
|
+
// Module-level calls (callerName === null) use the '<module>' sentinel emitted by
|
|
1102
|
+
// extractSpreadForOfWalk for top-level for-of loops. Look it up as a fallback so
|
|
1103
|
+
// that `for (const f of arr) { f(); }` at module scope resolves correctly.
|
|
1104
|
+
const modulePtsKey = caller.callerName === null && ptsMap?.has(`<module>::${call.name}`)
|
|
1105
|
+
? `<module>::${call.name}`
|
|
1106
|
+
: null;
|
|
1107
|
+
const flatPtsKey = !call.dynamic && fnRefBindingLhs.has(call.name) && ptsMap?.has(call.name) ? call.name : null;
|
|
1108
|
+
if (targets.length === 0 &&
|
|
1109
|
+
!call.receiver &&
|
|
1110
|
+
ptsMap &&
|
|
1111
|
+
(call.dynamic ||
|
|
1112
|
+
(scopedPtsKey != null && ptsMap.has(scopedPtsKey)) ||
|
|
1113
|
+
modulePtsKey != null ||
|
|
1114
|
+
flatPtsKey != null)) {
|
|
1115
|
+
const ptsLookupName = call.dynamic
|
|
1116
|
+
? call.name
|
|
1117
|
+
: scopedPtsKey != null && ptsMap.has(scopedPtsKey)
|
|
1118
|
+
? scopedPtsKey
|
|
1119
|
+
: modulePtsKey != null
|
|
1120
|
+
? modulePtsKey
|
|
1121
|
+
: // flatPtsKey != null is guaranteed by the outer if condition: if neither
|
|
1122
|
+
// call.dynamic nor scopedPtsKey nor modulePtsKey matched, flatPtsKey must be non-null.
|
|
1123
|
+
flatPtsKey;
|
|
1124
|
+
for (const alias of resolveViaPointsTo(ptsLookupName, ptsMap)) {
|
|
1125
|
+
// Resolve the concrete alias target. Only `name` is needed here — receiver
|
|
1126
|
+
// and line are not relevant for alias resolution (we are looking up the
|
|
1127
|
+
// aliased function by name, not dispatching a method call).
|
|
1128
|
+
const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
|
|
1129
|
+
for (const t of aliasTargets) {
|
|
1130
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
1131
|
+
if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
|
|
1132
|
+
const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
|
|
1133
|
+
if (conf > 0) {
|
|
1134
|
+
ptsEdgeRows.set(edgeKey, allEdgeRows.length);
|
|
1135
|
+
allEdgeRows.push([caller.id, t.id, 'calls', conf, isDynamic, 'points-to']);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
// Phase 8.3f: pts fallback for receiver calls via object-rest param bindings.
|
|
1142
|
+
// Fires when `rest.prop()` is encountered and `rest` was seeded as `pts["rest.prop"]`
|
|
1143
|
+
// by the object-rest dispatch chain (ObjectRestParamBinding + paramBinding + ObjectPropBinding).
|
|
1144
|
+
if (targets.length === 0 &&
|
|
1145
|
+
call.receiver &&
|
|
1146
|
+
!BUILTIN_RECEIVERS.has(call.receiver) &&
|
|
1147
|
+
call.receiver !== 'this' &&
|
|
1148
|
+
call.receiver !== 'self' &&
|
|
1149
|
+
call.receiver !== 'super' &&
|
|
1150
|
+
ptsMap) {
|
|
1151
|
+
const receiverKey = `${call.receiver}.${call.name}`;
|
|
1152
|
+
if (ptsMap.has(receiverKey)) {
|
|
1153
|
+
for (const alias of resolveViaPointsTo(receiverKey, ptsMap)) {
|
|
1154
|
+
const { targets: aliasTargets, importedFrom: aliasFrom } = resolveCallTargets(lookup, { name: alias }, relPath, importedNames, typeMap);
|
|
1155
|
+
for (const t of aliasTargets) {
|
|
1156
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
1157
|
+
if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
|
|
1158
|
+
const conf = computeConfidence(relPath, t.file, aliasFrom ?? null) - PROPAGATION_HOP_PENALTY;
|
|
1159
|
+
if (conf > 0) {
|
|
1160
|
+
ptsEdgeRows.set(edgeKey, allEdgeRows.length);
|
|
1161
|
+
allEdgeRows.push([caller.id, t.id, 'calls', conf, isDynamic, 'points-to']);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
385
1166
|
}
|
|
386
1167
|
}
|
|
387
1168
|
if (call.receiver &&
|
|
@@ -391,7 +1172,38 @@ function buildFileCallEdges(relPath, symbols, fileNodeRow, importedNames, seenCa
|
|
|
391
1172
|
call.receiver !== 'super') {
|
|
392
1173
|
const recv = resolveReceiverEdge(lookup, { name: call.name, receiver: call.receiver }, caller, relPath, typeMap, seenCallEdges);
|
|
393
1174
|
if (recv) {
|
|
394
|
-
allEdgeRows.push([recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0]);
|
|
1175
|
+
allEdgeRows.push([recv.callerId, recv.receiverId, 'receiver', recv.confidence, 0, null]);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
// Phase 8.5: CHA + RTA dispatch expansion.
|
|
1179
|
+
// For `this`/`self`/`super` calls: resolve through the class hierarchy instead
|
|
1180
|
+
// of relying solely on global name matching.
|
|
1181
|
+
// For typed receiver calls: expand to all instantiated concrete implementations.
|
|
1182
|
+
if (chaCtx && call.receiver) {
|
|
1183
|
+
let chaTargets = [];
|
|
1184
|
+
if (call.receiver === 'this' || call.receiver === 'self' || call.receiver === 'super') {
|
|
1185
|
+
chaTargets = resolveThisDispatch(call.name, caller.callerName, call.receiver, chaCtx, lookup);
|
|
1186
|
+
}
|
|
1187
|
+
else if (!BUILTIN_RECEIVERS.has(call.receiver)) {
|
|
1188
|
+
const typeEntry = typeMap.get(call.receiver);
|
|
1189
|
+
const typeName = typeEntry
|
|
1190
|
+
? typeof typeEntry === 'string'
|
|
1191
|
+
? typeEntry
|
|
1192
|
+
: typeEntry.type
|
|
1193
|
+
: null;
|
|
1194
|
+
if (typeName) {
|
|
1195
|
+
chaTargets = resolveChaTargets(typeName, call.name, chaCtx, lookup);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
for (const t of chaTargets) {
|
|
1199
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
1200
|
+
if (t.id !== caller.id && !seenCallEdges.has(edgeKey) && !ptsEdgeRows.has(edgeKey)) {
|
|
1201
|
+
const conf = computeConfidence(relPath, t.file, null) - CHA_DISPATCH_PENALTY;
|
|
1202
|
+
if (conf > 0) {
|
|
1203
|
+
seenCallEdges.add(edgeKey);
|
|
1204
|
+
allEdgeRows.push([caller.id, t.id, 'calls', conf, 0, 'cha']);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
395
1207
|
}
|
|
396
1208
|
}
|
|
397
1209
|
}
|
|
@@ -407,7 +1219,7 @@ function buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows) {
|
|
|
407
1219
|
const targetRows = (ctx.nodesByName.get(cls.extends) || []).filter((n) => EXTENDS_TARGET_KINDS.has(n.kind));
|
|
408
1220
|
if (sourceRow) {
|
|
409
1221
|
for (const t of targetRows) {
|
|
410
|
-
allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0]);
|
|
1222
|
+
allEdgeRows.push([sourceRow.id, t.id, 'extends', 1.0, 0, null]);
|
|
411
1223
|
}
|
|
412
1224
|
}
|
|
413
1225
|
}
|
|
@@ -416,12 +1228,46 @@ function buildClassHierarchyEdges(ctx, relPath, symbols, allEdgeRows) {
|
|
|
416
1228
|
const targetRows = (ctx.nodesByName.get(cls.implements) || []).filter((n) => IMPLEMENTS_TARGET_KINDS.has(n.kind));
|
|
417
1229
|
if (sourceRow) {
|
|
418
1230
|
for (const t of targetRows) {
|
|
419
|
-
allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0]);
|
|
1231
|
+
allEdgeRows.push([sourceRow.id, t.id, 'implements', 1.0, 0, null]);
|
|
420
1232
|
}
|
|
421
1233
|
}
|
|
422
1234
|
}
|
|
423
1235
|
}
|
|
424
1236
|
}
|
|
1237
|
+
// ── Native bulk-insert technique back-fill ──────────────────────────────
|
|
1238
|
+
/**
|
|
1239
|
+
* After native bulkInsertEdges (which does not write the technique column),
|
|
1240
|
+
* apply technique values from the in-memory row array back to the DB.
|
|
1241
|
+
*
|
|
1242
|
+
* Rows with an explicit technique get a targeted UPDATE by (source_id, target_id).
|
|
1243
|
+
* The catch-all 'ts-native' tag is scoped to only the source_ids present in this
|
|
1244
|
+
* batch — this prevents mis-tagging pre-migration NULL-technique edges from
|
|
1245
|
+
* unchanged files that were never purged and re-inserted.
|
|
1246
|
+
*/
|
|
1247
|
+
function applyEdgeTechniquesAfterNativeInsert(db, rows) {
|
|
1248
|
+
const callRows = rows.filter((r) => r[2] === 'calls');
|
|
1249
|
+
if (callRows.length === 0)
|
|
1250
|
+
return;
|
|
1251
|
+
const taggedRows = callRows.filter((r) => r[5] != null);
|
|
1252
|
+
// Collect distinct source IDs for this batch so the catch-all UPDATE is scoped
|
|
1253
|
+
// to edges inserted in the current run, not the entire table.
|
|
1254
|
+
const sourceIds = [...new Set(callRows.map((r) => r[0]))];
|
|
1255
|
+
// Chunk to stay within SQLite's SQLITE_LIMIT_VARIABLE_NUMBER (999 on older builds).
|
|
1256
|
+
const CHUNK_SIZE = 500;
|
|
1257
|
+
const tx = db.transaction(() => {
|
|
1258
|
+
if (taggedRows.length > 0) {
|
|
1259
|
+
const stmt = db.prepare("UPDATE edges SET technique = ? WHERE kind = 'calls' AND source_id = ? AND target_id = ? AND technique IS NULL");
|
|
1260
|
+
for (const r of taggedRows)
|
|
1261
|
+
stmt.run(r[5], r[0], r[1]);
|
|
1262
|
+
}
|
|
1263
|
+
for (let i = 0; i < sourceIds.length; i += CHUNK_SIZE) {
|
|
1264
|
+
const chunk = sourceIds.slice(i, i + CHUNK_SIZE);
|
|
1265
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
1266
|
+
db.prepare(`UPDATE edges SET technique = 'ts-native' WHERE kind = 'calls' AND technique IS NULL AND source_id IN (${placeholders})`).run(...chunk);
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
tx();
|
|
1270
|
+
}
|
|
425
1271
|
// ── Reverse-dep edge reconnection (#932, #933) ─────────────────────────
|
|
426
1272
|
/**
|
|
427
1273
|
* Reconnect edges that were saved before changed-file purge.
|
|
@@ -445,6 +1291,7 @@ function reconnectReverseDepEdges(ctx) {
|
|
|
445
1291
|
saved.edgeKind,
|
|
446
1292
|
saved.confidence,
|
|
447
1293
|
saved.dynamic,
|
|
1294
|
+
saved.technique,
|
|
448
1295
|
]);
|
|
449
1296
|
}
|
|
450
1297
|
else {
|
|
@@ -465,6 +1312,9 @@ function reconnectReverseDepEdges(ctx) {
|
|
|
465
1312
|
if (!ok) {
|
|
466
1313
|
batchInsertEdges(db, reconnectedRows);
|
|
467
1314
|
}
|
|
1315
|
+
else {
|
|
1316
|
+
applyEdgeTechniquesAfterNativeInsert(db, reconnectedRows);
|
|
1317
|
+
}
|
|
468
1318
|
}
|
|
469
1319
|
else {
|
|
470
1320
|
batchInsertEdges(db, reconnectedRows);
|
|
@@ -546,7 +1396,21 @@ export async function buildEdges(ctx) {
|
|
|
546
1396
|
setupNodeLookups(ctx, allNodesBefore);
|
|
547
1397
|
addLazyFallback(ctx, scopedLoad);
|
|
548
1398
|
const t0 = performance.now();
|
|
1399
|
+
// Enrich typeMap for .ts/.tsx files using the TypeScript compiler API.
|
|
1400
|
+
// Runs before call-edge construction so the accurate types are available
|
|
1401
|
+
// for method-call resolution. Gated on config so users can opt out.
|
|
1402
|
+
if (ctx.config.build.typescriptResolver) {
|
|
1403
|
+
await enrichTypeMapWithTsc(ctx.rootDir, ctx.fileSymbols);
|
|
1404
|
+
}
|
|
549
1405
|
const native = engineName === 'native' ? loadNative() : null;
|
|
1406
|
+
// Phase 8.2: Augment typeMaps with cross-file return-type propagation before
|
|
1407
|
+
// the transaction opens. This is pure in-memory mutation (no DB I/O) and must
|
|
1408
|
+
// run outside the transaction to avoid leaving ctx.fileSymbols in a partial
|
|
1409
|
+
// state if the transaction rolls back unexpectedly.
|
|
1410
|
+
propagateReturnTypesAcrossFiles(ctx.fileSymbols, ctx, ctx.rootDir);
|
|
1411
|
+
// Phase 8.5: Build CHA context after propagation so typeMap confidence values
|
|
1412
|
+
// (used for RTA seeding) reflect any cross-file propagated types.
|
|
1413
|
+
const chaCtx = buildChaContext(ctx.fileSymbols);
|
|
550
1414
|
// Phase 1: Compute edges inside a better-sqlite3 transaction.
|
|
551
1415
|
// Barrel-edge deletion lives here so that the JS path (which also inserts
|
|
552
1416
|
// edges in this transaction) keeps deletion + insertion atomic.
|
|
@@ -590,9 +1454,36 @@ export async function buildEdges(ctx) {
|
|
|
590
1454
|
(ctx.isFullBuild || ctx.fileSymbols.size > ctx.config.build.smallFilesThreshold);
|
|
591
1455
|
if (useNativeCallEdges) {
|
|
592
1456
|
buildCallEdgesNative(ctx, getNodeIdStmt, allEdgeRows, allNodesBefore, native);
|
|
1457
|
+
// Build the shared lookup once — both pts post-passes use it, avoiding
|
|
1458
|
+
// redundant construction of the same context closure.
|
|
1459
|
+
const sharedLookup = makeContextLookup(ctx, getNodeIdStmt);
|
|
1460
|
+
// Phase 8.3c post-pass: augment native call edges with parameter-flow pts
|
|
1461
|
+
// edges. The native Rust engine has no knowledge of paramBindings, so any
|
|
1462
|
+
// `fn()` call inside a higher-order function would be missed. This JS pass
|
|
1463
|
+
// runs on top of the native edges and adds only the pts-resolved edges that
|
|
1464
|
+
// the native engine could not produce.
|
|
1465
|
+
buildParamFlowPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
|
|
1466
|
+
// bind/alias post-pass: augment native call edges with fnRefBindings-seeded
|
|
1467
|
+
// pts edges. The native Rust engine has no knowledge of JS fnRefBindings
|
|
1468
|
+
// (e.g. `const f = fn.bind(ctx)`), so calls to bind-created aliases are
|
|
1469
|
+
// not resolved to their original function on the native path.
|
|
1470
|
+
buildFnRefBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
|
|
1471
|
+
// this-rebinding post-pass: resolve `this()` calls inside functions that
|
|
1472
|
+
// were invoked via `.call(namedCtx, ...)` / `.apply(namedCtx, ...)`.
|
|
1473
|
+
buildThisCallBindingsPtsPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
|
|
1474
|
+
// Phase 8.3f post-pass: augment native call edges with object rest-param
|
|
1475
|
+
// receiver resolution — typeMap[restName] → argName → typeMap[argName.method].
|
|
1476
|
+
buildObjectRestParamPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
|
|
1477
|
+
// Object.defineProperty accessor post-pass: resolve this-dispatch inside
|
|
1478
|
+
// getter/setter functions registered via Object.defineProperty.
|
|
1479
|
+
buildDefinePropertyPostPass(ctx, getNodeIdStmt, allEdgeRows, sharedLookup);
|
|
1480
|
+
// Phase 8.5 post-pass: augment native call edges with CHA-resolved dispatch.
|
|
1481
|
+
// The native Rust engine has no knowledge of the CHA context, so this/self
|
|
1482
|
+
// calls and interface dispatch are not expanded to concrete implementations.
|
|
1483
|
+
buildChaPostPass(ctx, getNodeIdStmt, allEdgeRows, chaCtx);
|
|
593
1484
|
}
|
|
594
1485
|
else {
|
|
595
|
-
buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows);
|
|
1486
|
+
buildCallEdgesJS(ctx, getNodeIdStmt, allEdgeRows, chaCtx);
|
|
596
1487
|
}
|
|
597
1488
|
// When using native edge insert, skip JS insert here — do it after tx commits.
|
|
598
1489
|
// Otherwise insert edges within this transaction for atomicity.
|
|
@@ -618,6 +1509,9 @@ export async function buildEdges(ctx) {
|
|
|
618
1509
|
debug('Native bulkInsertEdges failed — falling back to JS batchInsertEdges');
|
|
619
1510
|
batchInsertEdges(ctx.db, allEdgeRows);
|
|
620
1511
|
}
|
|
1512
|
+
else {
|
|
1513
|
+
applyEdgeTechniquesAfterNativeInsert(ctx.db, allEdgeRows);
|
|
1514
|
+
}
|
|
621
1515
|
}
|
|
622
1516
|
// Phase 3: Reconnect saved reverse-dep edges (#932, #933).
|
|
623
1517
|
// When the WASM/JS path purged changed files, edges FROM reverse-dep files TO
|
|
@@ -627,6 +1521,12 @@ export async function buildEdges(ctx) {
|
|
|
627
1521
|
if (ctx.savedReverseDepEdges.length > 0) {
|
|
628
1522
|
reconnectReverseDepEdges(ctx);
|
|
629
1523
|
}
|
|
1524
|
+
// Phase 4: CHA post-pass — expand virtual-dispatch edges for class hierarchies
|
|
1525
|
+
// and interface implementations. Runs after all call + hierarchy edges are
|
|
1526
|
+
// committed so the DB is consistent.
|
|
1527
|
+
// Note: the native orchestrator success path runs this independently in
|
|
1528
|
+
// tryNativeOrchestrator; this phase covers the WASM and native-fallback paths.
|
|
1529
|
+
runChaPostPass(db);
|
|
630
1530
|
ctx.timing.edgesMs = performance.now() - t0;
|
|
631
1531
|
}
|
|
632
1532
|
//# sourceMappingURL=build-edges.js.map
|