@zuvia-software-solutions/code-mapper 1.4.0 → 2.0.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/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.d.ts +1 -0
- package/dist/cli/analyze.js +73 -82
- package/dist/cli/augment.js +0 -2
- package/dist/cli/eval-server.d.ts +2 -2
- package/dist/cli/eval-server.js +6 -6
- package/dist/cli/index.js +6 -10
- package/dist/cli/mcp.d.ts +1 -3
- package/dist/cli/mcp.js +3 -3
- package/dist/cli/refresh.d.ts +2 -2
- package/dist/cli/refresh.js +24 -29
- package/dist/cli/status.js +4 -13
- package/dist/cli/tool.d.ts +5 -4
- package/dist/cli/tool.js +8 -10
- package/dist/config/ignore-service.js +14 -34
- package/dist/core/augmentation/engine.js +53 -83
- package/dist/core/db/adapter.d.ts +99 -0
- package/dist/core/db/adapter.js +402 -0
- package/dist/core/db/graph-loader.d.ts +27 -0
- package/dist/core/db/graph-loader.js +148 -0
- package/dist/core/db/queries.d.ts +160 -0
- package/dist/core/db/queries.js +441 -0
- package/dist/core/db/schema.d.ts +108 -0
- package/dist/core/db/schema.js +136 -0
- package/dist/core/embeddings/embedder.d.ts +21 -12
- package/dist/core/embeddings/embedder.js +104 -50
- package/dist/core/embeddings/embedding-pipeline.d.ts +48 -22
- package/dist/core/embeddings/embedding-pipeline.js +220 -262
- package/dist/core/embeddings/text-generator.js +4 -19
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/graph.d.ts +1 -1
- package/dist/core/graph/graph.js +1 -0
- package/dist/core/graph/types.d.ts +11 -9
- package/dist/core/graph/types.js +4 -1
- package/dist/core/incremental/refresh.d.ts +46 -0
- package/dist/core/incremental/refresh.js +464 -0
- package/dist/core/incremental/types.d.ts +2 -1
- package/dist/core/incremental/types.js +42 -44
- package/dist/core/ingestion/ast-cache.js +1 -0
- package/dist/core/ingestion/call-processor.d.ts +15 -3
- package/dist/core/ingestion/call-processor.js +448 -60
- package/dist/core/ingestion/cluster-enricher.d.ts +1 -1
- package/dist/core/ingestion/cluster-enricher.js +2 -0
- package/dist/core/ingestion/community-processor.d.ts +1 -1
- package/dist/core/ingestion/community-processor.js +8 -3
- package/dist/core/ingestion/export-detection.d.ts +1 -1
- package/dist/core/ingestion/export-detection.js +1 -1
- package/dist/core/ingestion/filesystem-walker.js +1 -1
- package/dist/core/ingestion/heritage-processor.d.ts +2 -2
- package/dist/core/ingestion/heritage-processor.js +22 -11
- package/dist/core/ingestion/import-processor.d.ts +2 -2
- package/dist/core/ingestion/import-processor.js +24 -9
- package/dist/core/ingestion/language-config.js +7 -4
- package/dist/core/ingestion/mro-processor.d.ts +1 -1
- package/dist/core/ingestion/mro-processor.js +23 -11
- package/dist/core/ingestion/named-binding-extraction.js +5 -5
- package/dist/core/ingestion/parsing-processor.d.ts +4 -4
- package/dist/core/ingestion/parsing-processor.js +26 -18
- package/dist/core/ingestion/pipeline.d.ts +4 -2
- package/dist/core/ingestion/pipeline.js +50 -20
- package/dist/core/ingestion/process-processor.d.ts +2 -2
- package/dist/core/ingestion/process-processor.js +28 -14
- package/dist/core/ingestion/resolution-context.d.ts +1 -1
- package/dist/core/ingestion/resolution-context.js +14 -4
- package/dist/core/ingestion/resolvers/csharp.js +4 -3
- package/dist/core/ingestion/resolvers/go.js +3 -1
- package/dist/core/ingestion/resolvers/jvm.js +13 -4
- package/dist/core/ingestion/resolvers/standard.js +2 -2
- package/dist/core/ingestion/resolvers/utils.js +6 -2
- package/dist/core/ingestion/route-stitcher.d.ts +15 -0
- package/dist/core/ingestion/route-stitcher.js +92 -0
- package/dist/core/ingestion/structure-processor.d.ts +1 -1
- package/dist/core/ingestion/structure-processor.js +3 -2
- package/dist/core/ingestion/symbol-table.d.ts +2 -0
- package/dist/core/ingestion/symbol-table.js +5 -1
- package/dist/core/ingestion/tree-sitter-queries.d.ts +2 -2
- package/dist/core/ingestion/tree-sitter-queries.js +177 -0
- package/dist/core/ingestion/type-env.js +20 -0
- package/dist/core/ingestion/type-extractors/csharp.js +4 -3
- package/dist/core/ingestion/type-extractors/go.js +23 -12
- package/dist/core/ingestion/type-extractors/php.js +18 -10
- package/dist/core/ingestion/type-extractors/ruby.js +15 -3
- package/dist/core/ingestion/type-extractors/rust.js +3 -2
- package/dist/core/ingestion/type-extractors/shared.js +3 -2
- package/dist/core/ingestion/type-extractors/typescript.js +11 -5
- package/dist/core/ingestion/utils.d.ts +27 -4
- package/dist/core/ingestion/utils.js +145 -100
- package/dist/core/ingestion/workers/parse-worker.d.ts +1 -0
- package/dist/core/ingestion/workers/parse-worker.js +97 -29
- package/dist/core/ingestion/workers/worker-pool.js +3 -0
- package/dist/core/search/bm25-index.d.ts +15 -8
- package/dist/core/search/bm25-index.js +48 -98
- package/dist/core/search/hybrid-search.d.ts +9 -3
- package/dist/core/search/hybrid-search.js +30 -25
- package/dist/core/search/reranker.js +9 -7
- package/dist/core/search/types.d.ts +0 -4
- package/dist/core/semantic/tsgo-service.d.ts +5 -1
- package/dist/core/semantic/tsgo-service.js +161 -66
- package/dist/lib/tsgo-test.d.ts +2 -0
- package/dist/lib/tsgo-test.js +6 -0
- package/dist/lib/type-utils.d.ts +25 -0
- package/dist/lib/type-utils.js +22 -0
- package/dist/lib/utils.d.ts +3 -2
- package/dist/lib/utils.js +3 -2
- package/dist/mcp/compatible-stdio-transport.js +1 -1
- package/dist/mcp/local/local-backend.d.ts +29 -56
- package/dist/mcp/local/local-backend.js +808 -1118
- package/dist/mcp/resources.js +35 -25
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +5 -5
- package/dist/mcp/tools.js +24 -25
- package/dist/storage/repo-manager.d.ts +2 -12
- package/dist/storage/repo-manager.js +1 -47
- package/dist/types/pipeline.d.ts +8 -5
- package/dist/types/pipeline.js +5 -0
- package/package.json +18 -11
- package/dist/cli/serve.d.ts +0 -5
- package/dist/cli/serve.js +0 -8
- package/dist/core/incremental/child-process.d.ts +0 -8
- package/dist/core/incremental/child-process.js +0 -649
- package/dist/core/incremental/refresh-coordinator.d.ts +0 -32
- package/dist/core/incremental/refresh-coordinator.js +0 -147
- package/dist/core/lbug/csv-generator.d.ts +0 -28
- package/dist/core/lbug/csv-generator.js +0 -355
- package/dist/core/lbug/lbug-adapter.d.ts +0 -96
- package/dist/core/lbug/lbug-adapter.js +0 -753
- package/dist/core/lbug/schema.d.ts +0 -46
- package/dist/core/lbug/schema.js +0 -402
- package/dist/mcp/core/embedder.d.ts +0 -24
- package/dist/mcp/core/embedder.js +0 -168
- package/dist/mcp/core/lbug-adapter.d.ts +0 -29
- package/dist/mcp/core/lbug-adapter.js +0 -330
- package/dist/server/api.d.ts +0 -5
- package/dist/server/api.js +0 -340
- package/dist/server/mcp-http.d.ts +0 -7
- package/dist/server/mcp-http.js +0 -95
- package/models/mlx-embedder.py +0 -185
|
@@ -5,10 +5,12 @@ import { TIER_CONFIDENCE } from './resolution-context.js';
|
|
|
5
5
|
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
6
6
|
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
|
|
7
7
|
import { generateId } from '../../lib/utils.js';
|
|
8
|
+
import { toNodeId, toEdgeId } from '../db/schema.js';
|
|
8
9
|
import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, CALL_EXPRESSION_TYPES, extractCallChain, } from './utils.js';
|
|
9
10
|
import { buildTypeEnv } from './type-env.js';
|
|
10
11
|
import { getTreeSitterBufferSize } from './constants.js';
|
|
11
12
|
import { callRouters } from './call-routing.js';
|
|
13
|
+
import path from 'node:path';
|
|
12
14
|
/** Walk up the AST to find the enclosing function/method, or null for top-level code */
|
|
13
15
|
const findEnclosingFunction = (node, filePath, ctx) => {
|
|
14
16
|
let current = node.parent;
|
|
@@ -18,7 +20,9 @@ const findEnclosingFunction = (node, filePath, ctx) => {
|
|
|
18
20
|
if (funcName) {
|
|
19
21
|
const resolved = ctx.resolve(funcName, filePath);
|
|
20
22
|
if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
|
|
21
|
-
|
|
23
|
+
const first = resolved.candidates[0];
|
|
24
|
+
if (first)
|
|
25
|
+
return first.nodeId;
|
|
22
26
|
}
|
|
23
27
|
return generateId(label, `${filePath}:${funcName}`);
|
|
24
28
|
}
|
|
@@ -46,7 +50,7 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
|
|
|
46
50
|
const narrowed = callableDefs.filter(d => {
|
|
47
51
|
if (!d.ownerId)
|
|
48
52
|
return false;
|
|
49
|
-
const owner = graph.getNode(d.ownerId);
|
|
53
|
+
const owner = graph.getNode(toNodeId(d.ownerId));
|
|
50
54
|
return owner?.properties.name === receiverClassName;
|
|
51
55
|
});
|
|
52
56
|
if (narrowed.length > 0)
|
|
@@ -63,8 +67,9 @@ const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
|
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
}
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
const singleDef = callableDefs && callableDefs.length === 1 ? callableDefs[0] : undefined;
|
|
71
|
+
if (singleDef && singleDef.returnType) {
|
|
72
|
+
const typeName = extractReturnTypeName(singleDef.returnType);
|
|
68
73
|
if (typeName) {
|
|
69
74
|
verified.set(receiverKey(scope, varName), typeName);
|
|
70
75
|
}
|
|
@@ -105,6 +110,8 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
105
110
|
const skippedByLang = logSkipped ? new Map() : null;
|
|
106
111
|
for (let i = 0; i < files.length; i++) {
|
|
107
112
|
const file = files[i];
|
|
113
|
+
if (!file)
|
|
114
|
+
continue;
|
|
108
115
|
onProgress?.(i + 1, files.length);
|
|
109
116
|
if (i % 20 === 0)
|
|
110
117
|
await yieldToEventLoop();
|
|
@@ -190,15 +197,16 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
190
197
|
},
|
|
191
198
|
});
|
|
192
199
|
ctx.symbols.add(file.path, item.propName, nodeId, 'Property', propEnclosingClassId ? { ownerId: propEnclosingClassId } : undefined);
|
|
193
|
-
const relId =
|
|
200
|
+
const relId = toEdgeId(`DEFINES:${fileId}->${nodeId}`);
|
|
194
201
|
graph.addRelationship({
|
|
195
202
|
id: relId, sourceId: fileId, targetId: nodeId,
|
|
196
203
|
type: 'DEFINES', confidence: 1.0, reason: '',
|
|
197
204
|
});
|
|
198
205
|
if (propEnclosingClassId) {
|
|
206
|
+
const ownerNodeId = toNodeId(propEnclosingClassId);
|
|
199
207
|
graph.addRelationship({
|
|
200
|
-
id:
|
|
201
|
-
sourceId:
|
|
208
|
+
id: toEdgeId(`HAS_METHOD:${propEnclosingClassId}->${nodeId}`),
|
|
209
|
+
sourceId: ownerNodeId, targetId: nodeId,
|
|
202
210
|
type: 'HAS_METHOD', confidence: 1.0, reason: '',
|
|
203
211
|
});
|
|
204
212
|
}
|
|
@@ -239,7 +247,7 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
239
247
|
const importedName = child.text;
|
|
240
248
|
if (importedName && !isBuiltInOrNoise(importedName)) {
|
|
241
249
|
const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
|
|
242
|
-
const sourceId = enclosingFuncId
|
|
250
|
+
const sourceId = enclosingFuncId ? toNodeId(enclosingFuncId) : generateId('File', file.path);
|
|
243
251
|
// Try module-scoped resolution first: extract the target filename
|
|
244
252
|
// from the import path and look up the name in that file
|
|
245
253
|
let targetId = null;
|
|
@@ -261,16 +269,19 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
261
269
|
if (!targetId) {
|
|
262
270
|
const resolved = ctx.resolve(importedName, file.path);
|
|
263
271
|
if (resolved && resolved.candidates.length > 0) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
272
|
+
const firstCandidate = resolved.candidates[0];
|
|
273
|
+
if (firstCandidate) {
|
|
274
|
+
targetId = firstCandidate.nodeId;
|
|
275
|
+
confidence = Math.max(TIER_CONFIDENCE[resolved.tier] ?? 0.5, 0.8);
|
|
276
|
+
reason = `dynamic-import:${resolved.tier}`;
|
|
277
|
+
}
|
|
267
278
|
}
|
|
268
279
|
}
|
|
269
280
|
if (targetId) {
|
|
270
281
|
graph.addRelationship({
|
|
271
|
-
id:
|
|
282
|
+
id: toEdgeId(`CALLS:${sourceId}:dyn_import:${importedName}->${targetId}`),
|
|
272
283
|
sourceId,
|
|
273
|
-
targetId,
|
|
284
|
+
targetId: toNodeId(targetId),
|
|
274
285
|
type: 'CALLS',
|
|
275
286
|
confidence,
|
|
276
287
|
reason,
|
|
@@ -336,22 +347,28 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
336
347
|
const GENERIC_MEMBER_METHODS = new Set(['has', 'get', 'set', 'add', 'remove', 'delete', 'close', 'stop', 'clear', 'reset', 'toString', 'valueOf', 'push', 'pop', 'entries', 'keys', 'values']);
|
|
337
348
|
if (callForm === 'member' && !receiverTypeName && GENERIC_MEMBER_METHODS.has(calledName))
|
|
338
349
|
return;
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
350
|
+
const argCount = countCallArguments(callNode);
|
|
351
|
+
const callInfo = { calledName };
|
|
352
|
+
if (argCount !== undefined)
|
|
353
|
+
callInfo.argCount = argCount;
|
|
354
|
+
if (callForm !== undefined)
|
|
355
|
+
callInfo.callForm = callForm;
|
|
356
|
+
if (receiverTypeName !== undefined)
|
|
357
|
+
callInfo.receiverTypeName = receiverTypeName;
|
|
358
|
+
const resolved = resolveCallTarget(callInfo, file.path, ctx);
|
|
345
359
|
if (!resolved)
|
|
346
360
|
return;
|
|
347
361
|
const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
|
|
348
|
-
const sourceId = enclosingFuncId
|
|
349
|
-
|
|
362
|
+
const sourceId = enclosingFuncId ? toNodeId(enclosingFuncId) : generateId('File', file.path);
|
|
363
|
+
// Suppress self-referencing edges from unresolved method chains
|
|
364
|
+
if (sourceId === toNodeId(resolved.nodeId) && callForm === 'member' && !receiverTypeName)
|
|
365
|
+
return;
|
|
366
|
+
const relId = toEdgeId(`CALLS:${sourceId}:${calledName}->${resolved.nodeId}`);
|
|
350
367
|
const callContext = isInsideCatch(callNode) ? 'error-handler' : isInsideConditional(callNode) ? 'conditional' : '';
|
|
351
368
|
graph.addRelationship({
|
|
352
369
|
id: relId,
|
|
353
370
|
sourceId,
|
|
354
|
-
targetId: resolved.nodeId,
|
|
371
|
+
targetId: toNodeId(resolved.nodeId),
|
|
355
372
|
type: 'CALLS',
|
|
356
373
|
confidence: resolved.confidence,
|
|
357
374
|
reason: callContext || resolved.reason,
|
|
@@ -387,13 +404,15 @@ export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
|
387
404
|
const importedName = child.text;
|
|
388
405
|
if (importedName && !isBuiltInOrNoise(importedName)) {
|
|
389
406
|
const enclosingFuncId = findEnclosingFunction(node, file.path, ctx);
|
|
390
|
-
const sourceId = enclosingFuncId
|
|
407
|
+
const sourceId = enclosingFuncId ? toNodeId(enclosingFuncId) : generateId('File', file.path);
|
|
391
408
|
const resolved = ctx.resolve(importedName, file.path);
|
|
392
|
-
|
|
409
|
+
const firstResolved = resolved && resolved.candidates.length > 0 ? resolved.candidates[0] : undefined;
|
|
410
|
+
if (firstResolved) {
|
|
411
|
+
const targetNodeId = toNodeId(firstResolved.nodeId);
|
|
393
412
|
graph.addRelationship({
|
|
394
|
-
id:
|
|
413
|
+
id: toEdgeId(`CALLS:${sourceId}:dyn:${importedName}->${targetNodeId}`),
|
|
395
414
|
sourceId,
|
|
396
|
-
targetId:
|
|
415
|
+
targetId: targetNodeId,
|
|
397
416
|
type: 'CALLS',
|
|
398
417
|
confidence: 0.85,
|
|
399
418
|
reason: 'dynamic-import',
|
|
@@ -476,7 +495,10 @@ const toResolveResult = (definition, tier) => ({
|
|
|
476
495
|
function resolveChainedReceiver(chainNames, baseReceiverTypeName, currentFile, ctx) {
|
|
477
496
|
let currentType = baseReceiverTypeName;
|
|
478
497
|
for (const name of chainNames) {
|
|
479
|
-
const
|
|
498
|
+
const chainCallInfo = { calledName: name, callForm: 'member' };
|
|
499
|
+
if (currentType !== undefined)
|
|
500
|
+
chainCallInfo.receiverTypeName = currentType;
|
|
501
|
+
const resolved = resolveCallTarget(chainCallInfo, currentFile, ctx);
|
|
480
502
|
if (!resolved)
|
|
481
503
|
return undefined;
|
|
482
504
|
const candidates = ctx.symbols.lookupFuzzy(name);
|
|
@@ -516,14 +538,16 @@ const resolveCallTarget = (call, currentFile, ctx) => {
|
|
|
516
538
|
: filteredCandidates;
|
|
517
539
|
// D3. File-based: prefer candidates in the resolved type's file
|
|
518
540
|
const fileFiltered = methodPool.filter(c => typeFiles.has(c.filePath));
|
|
519
|
-
|
|
520
|
-
|
|
541
|
+
const firstFileFiltered = fileFiltered.length === 1 ? fileFiltered[0] : undefined;
|
|
542
|
+
if (firstFileFiltered) {
|
|
543
|
+
return toResolveResult(firstFileFiltered, tiered.tier);
|
|
521
544
|
}
|
|
522
545
|
// D4. ownerId fallback: narrow by ownerId matching type's nodeId
|
|
523
546
|
const pool = fileFiltered.length > 0 ? fileFiltered : methodPool;
|
|
524
547
|
const ownerFiltered = pool.filter(c => c.ownerId && typeNodeIds.has(c.ownerId));
|
|
525
|
-
|
|
526
|
-
|
|
548
|
+
const firstOwnerFiltered = ownerFiltered.length === 1 ? ownerFiltered[0] : undefined;
|
|
549
|
+
if (firstOwnerFiltered) {
|
|
550
|
+
return toResolveResult(firstOwnerFiltered, tiered.tier);
|
|
527
551
|
}
|
|
528
552
|
if (fileFiltered.length > 1 || ownerFiltered.length > 1)
|
|
529
553
|
return null;
|
|
@@ -534,13 +558,17 @@ const resolveCallTarget = (call, currentFile, ctx) => {
|
|
|
534
558
|
// Deduplicate by nodeId — the same symbol can appear multiple times from different resolution paths
|
|
535
559
|
if (filteredCandidates.length > 1) {
|
|
536
560
|
const uniqueById = new Map(filteredCandidates.map(c => [c.nodeId, c]));
|
|
537
|
-
|
|
538
|
-
|
|
561
|
+
const firstUnique = uniqueById.size === 1 ? filteredCandidates[0] : undefined;
|
|
562
|
+
if (firstUnique) {
|
|
563
|
+
return toResolveResult(firstUnique, tiered.tier);
|
|
539
564
|
}
|
|
540
565
|
// Multiple distinct candidates — ambiguous, skip
|
|
541
566
|
return null;
|
|
542
567
|
}
|
|
543
|
-
|
|
568
|
+
const firstCandidate = filteredCandidates[0];
|
|
569
|
+
if (!firstCandidate)
|
|
570
|
+
return null;
|
|
571
|
+
return toResolveResult(firstCandidate, tiered.tier);
|
|
544
572
|
};
|
|
545
573
|
// Return type text helpers
|
|
546
574
|
// Operates on raw return-type text in SymbolDefinition (e.g. "User", "Promise<User>",
|
|
@@ -612,15 +640,19 @@ export const extractReturnTypeName = (raw, depth = 0) => {
|
|
|
612
640
|
// Handle union types ("User | null" -> "User")
|
|
613
641
|
if (text.includes('|')) {
|
|
614
642
|
const parts = text.split('|').map(p => p.trim()).filter(p => p !== 'null' && p !== 'undefined' && p !== 'void' && p !== 'None' && p !== 'nil');
|
|
615
|
-
|
|
616
|
-
|
|
643
|
+
const singlePart = parts.length === 1 ? parts[0] : undefined;
|
|
644
|
+
if (singlePart)
|
|
645
|
+
text = singlePart;
|
|
617
646
|
else
|
|
618
647
|
return undefined; // genuine union, too complex
|
|
619
648
|
}
|
|
620
649
|
// Handle generics (Promise<User> -> unwrap if wrapper, else take base)
|
|
621
650
|
const genericMatch = text.match(/^(\w+)\s*<(.+)>$/);
|
|
622
651
|
if (genericMatch) {
|
|
623
|
-
const
|
|
652
|
+
const base = genericMatch[1];
|
|
653
|
+
const args = genericMatch[2];
|
|
654
|
+
if (!base || !args)
|
|
655
|
+
return undefined;
|
|
624
656
|
if (WRAPPER_GENERICS.has(base)) {
|
|
625
657
|
// Take the first non-lifetime type argument using bracket-balanced splitting
|
|
626
658
|
const firstArg = extractFirstTypeArg(args);
|
|
@@ -634,7 +666,10 @@ export const extractReturnTypeName = (raw, depth = 0) => {
|
|
|
634
666
|
return undefined;
|
|
635
667
|
// Handle qualified names (models.User -> User, Models::User -> User)
|
|
636
668
|
if (text.includes('::') || text.includes('.') || text.includes('\\')) {
|
|
637
|
-
|
|
669
|
+
const lastSegment = text.split(/::|[.\\]/).pop();
|
|
670
|
+
if (!lastSegment)
|
|
671
|
+
return undefined;
|
|
672
|
+
text = lastSegment;
|
|
638
673
|
}
|
|
639
674
|
// Skip primitives
|
|
640
675
|
if (PRIMITIVE_TYPES.has(text) || PRIMITIVE_TYPES.has(text.toLowerCase()))
|
|
@@ -649,8 +684,6 @@ export const extractReturnTypeName = (raw, depth = 0) => {
|
|
|
649
684
|
// Source IDs: "Label:filepath:funcName" (parse-worker.ts)
|
|
650
685
|
// NUL (\0) separates composite keys to prevent ambiguous concatenation
|
|
651
686
|
// receiverKey uses full scope to distinguish overloaded methods (e.g. User.save@100 vs Repo.save@200)
|
|
652
|
-
/** Extract function name from a scope key ("funcName@startIndex" -> "funcName") */
|
|
653
|
-
const extractFuncNameFromScope = (scope) => scope.slice(0, scope.indexOf('@'));
|
|
654
687
|
/** Extract trailing function name from a sourceId ("Function:filepath:funcName" -> "funcName") */
|
|
655
688
|
const extractFuncNameFromSourceId = (sourceId) => {
|
|
656
689
|
const lastColon = sourceId.lastIndexOf(':');
|
|
@@ -694,8 +727,128 @@ const lookupReceiverType = (map, funcName, varName) => {
|
|
|
694
727
|
// Fallback: file-level scope
|
|
695
728
|
return map.get(fileLevelKey);
|
|
696
729
|
};
|
|
730
|
+
/** Check if a file is TypeScript or JavaScript (extensions tsgo can handle) */
|
|
731
|
+
function isTypeScriptOrJavaScript(filePath) {
|
|
732
|
+
return /\.(ts|tsx|js|jsx|mts|mjs|cts|cjs)$/.test(filePath);
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Batch-resolve call sites via tsgo LSP before heuristic resolution.
|
|
736
|
+
*
|
|
737
|
+
* For each TS/JS call with line+column info, asks tsgo for go-to-definition.
|
|
738
|
+
* Returns a Map from callKey -> ResolveResult for calls that tsgo resolved.
|
|
739
|
+
*
|
|
740
|
+
* Call key format: "sourceId\0calledName\0callLine" — unique per call site.
|
|
741
|
+
*/
|
|
742
|
+
async function batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPath) {
|
|
743
|
+
const results = new Map();
|
|
744
|
+
// Collect eligible calls (TS/JS files with line+column info)
|
|
745
|
+
const eligible = [];
|
|
746
|
+
for (const call of extractedCalls) {
|
|
747
|
+
if (isTypeScriptOrJavaScript(call.filePath) &&
|
|
748
|
+
call.callLine !== undefined &&
|
|
749
|
+
call.callColumn !== undefined) {
|
|
750
|
+
eligible.push(call);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (eligible.length === 0)
|
|
754
|
+
return results;
|
|
755
|
+
// Group calls by file — process one file at a time so tsgo only needs
|
|
756
|
+
// one file hot in memory. LSP is sequential over stdio, so concurrent
|
|
757
|
+
// requests just create a queue that causes timeouts.
|
|
758
|
+
const byFile = new Map();
|
|
759
|
+
for (const call of eligible) {
|
|
760
|
+
let list = byFile.get(call.filePath);
|
|
761
|
+
if (!list) {
|
|
762
|
+
list = [];
|
|
763
|
+
byFile.set(call.filePath, list);
|
|
764
|
+
}
|
|
765
|
+
list.push(call);
|
|
766
|
+
}
|
|
767
|
+
let resolved = 0;
|
|
768
|
+
let failed = 0;
|
|
769
|
+
const t0 = Date.now();
|
|
770
|
+
console.error(`Code Mapper: tsgo resolving ${eligible.length} calls across ${byFile.size} files...`);
|
|
771
|
+
for (const [filePath, calls] of byFile) {
|
|
772
|
+
const absFilePath = path.resolve(repoPath, filePath);
|
|
773
|
+
// Resolve all calls in this file sequentially
|
|
774
|
+
for (const call of calls) {
|
|
775
|
+
try {
|
|
776
|
+
const def = await tsgoService.resolveDefinition(absFilePath, call.callLine - 1, call.callColumn);
|
|
777
|
+
if (!def) {
|
|
778
|
+
failed++;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
const targetSymbols = ctx.symbols.lookupAllInFile(def.filePath);
|
|
782
|
+
if (targetSymbols.length === 0) {
|
|
783
|
+
failed++;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
// Match by exact startLine, then by range containment
|
|
787
|
+
let bestMatch;
|
|
788
|
+
for (const sym of targetSymbols) {
|
|
789
|
+
const node = graph.getNode(toNodeId(sym.nodeId));
|
|
790
|
+
if (node && node.properties.startLine === def.line) {
|
|
791
|
+
bestMatch = sym;
|
|
792
|
+
break;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (!bestMatch) {
|
|
796
|
+
for (const sym of targetSymbols) {
|
|
797
|
+
const node = graph.getNode(toNodeId(sym.nodeId));
|
|
798
|
+
if (node) {
|
|
799
|
+
const sl = node.properties.startLine;
|
|
800
|
+
const el = node.properties.endLine;
|
|
801
|
+
if (sl !== undefined && el !== undefined && def.line >= sl && def.line <= el) {
|
|
802
|
+
bestMatch = sym;
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (bestMatch) {
|
|
809
|
+
// Drop self-referencing tsgo edges: these come from property access
|
|
810
|
+
// on parameters (req.params, res.json) that tsgo resolves back to
|
|
811
|
+
// the enclosing function's definition. Legitimate recursion is captured
|
|
812
|
+
// by the heuristic path (free-form calls to the function's own name).
|
|
813
|
+
if (bestMatch.nodeId === call.sourceId) {
|
|
814
|
+
failed++;
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
const callKey = `${call.sourceId}\0${call.calledName}\0${call.callLine}`;
|
|
818
|
+
results.set(callKey, {
|
|
819
|
+
nodeId: bestMatch.nodeId,
|
|
820
|
+
confidence: TIER_CONFIDENCE['tsgo-resolved'],
|
|
821
|
+
reason: 'tsgo-lsp',
|
|
822
|
+
});
|
|
823
|
+
resolved++;
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
failed++;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
failed++;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
const elapsed = Date.now() - t0;
|
|
835
|
+
console.error(`Code Mapper: tsgo resolved ${resolved}/${eligible.length} calls in ${elapsed}ms (${failed} unresolvable)`);
|
|
836
|
+
return results;
|
|
837
|
+
}
|
|
838
|
+
/** Generic method names that produce false edges when receiver type is unknown (worker-extracted path) */
|
|
839
|
+
const GENERIC_MEMBER_METHODS_WORKER = new Set([
|
|
840
|
+
'has', 'get', 'set', 'add', 'remove', 'delete', 'close', 'stop', 'clear', 'reset',
|
|
841
|
+
'toString', 'valueOf', 'push', 'pop', 'entries', 'keys', 'values',
|
|
842
|
+
'all', 'any', 'race', 'resolve', 'reject', // Promise methods
|
|
843
|
+
'map', 'filter', 'reduce', 'forEach', 'find', 'some', 'every', 'includes', // Array methods
|
|
844
|
+
'then', 'catch', 'finally', // Promise chain
|
|
845
|
+
'next', 'return', 'throw', // Iterator
|
|
846
|
+
'emit', 'on', 'off', 'once', 'subscribe', 'unsubscribe', // EventEmitter
|
|
847
|
+
'log', 'warn', 'error', 'info', 'debug', // Console
|
|
848
|
+
'json', 'text', 'blob', 'status', 'send', 'end', // HTTP response
|
|
849
|
+
]);
|
|
697
850
|
/** Resolve pre-extracted call sites from workers (no AST parsing needed) */
|
|
698
|
-
export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings) => {
|
|
851
|
+
export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings, tsgoService, repoPath) => {
|
|
699
852
|
// Scope-aware receiver types keyed by filePath -> scope\0varName -> typeName
|
|
700
853
|
const fileReceiverTypes = new Map();
|
|
701
854
|
if (constructorBindings) {
|
|
@@ -715,6 +868,11 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
715
868
|
}
|
|
716
869
|
list.push(call);
|
|
717
870
|
}
|
|
871
|
+
// Batch pre-resolve via tsgo LSP (highest confidence, TS/JS only)
|
|
872
|
+
let tsgoResolved;
|
|
873
|
+
if (tsgoService?.isReady() && repoPath) {
|
|
874
|
+
tsgoResolved = await batchResolveTsgo(tsgoService, extractedCalls, ctx, graph, repoPath);
|
|
875
|
+
}
|
|
718
876
|
const totalFiles = byFile.size;
|
|
719
877
|
let filesProcessed = 0;
|
|
720
878
|
for (const [filePath, calls] of byFile) {
|
|
@@ -758,19 +916,172 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
758
916
|
effectiveCall = { ...effectiveCall, receiverTypeName: chainedType };
|
|
759
917
|
}
|
|
760
918
|
}
|
|
761
|
-
|
|
919
|
+
// Skip generic method names on unknown receivers (matches sequential path filter)
|
|
920
|
+
// Prevents false edges like Promise.all() → bookingKeys.all()
|
|
921
|
+
if (effectiveCall.callForm === 'member' && !effectiveCall.receiverTypeName && GENERIC_MEMBER_METHODS_WORKER.has(effectiveCall.calledName)) {
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
// Check tsgo pre-resolved map first (highest confidence)
|
|
925
|
+
let resolved;
|
|
926
|
+
if (tsgoResolved && effectiveCall.callLine !== undefined) {
|
|
927
|
+
const callKey = `${effectiveCall.sourceId}\0${effectiveCall.calledName}\0${effectiveCall.callLine}`;
|
|
928
|
+
resolved = tsgoResolved.get(callKey);
|
|
929
|
+
}
|
|
930
|
+
// Fall through to heuristic resolution if tsgo didn't resolve
|
|
931
|
+
if (!resolved) {
|
|
932
|
+
resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
|
|
933
|
+
}
|
|
934
|
+
// RC-I: Interface dispatch — when receiver type is an Interface, find implementations.
|
|
935
|
+
// Strategy 1: Classes with IMPLEMENTS edges to the interface.
|
|
936
|
+
// Strategy 2: Factory functions whose return type matches the interface name.
|
|
937
|
+
// Covers both class-based and closure-based (factory pattern) implementations.
|
|
938
|
+
if (!resolved && effectiveCall.callForm === 'member' && effectiveCall.receiverTypeName) {
|
|
939
|
+
const receiverResolved = ctx.resolve(effectiveCall.receiverTypeName, effectiveCall.filePath);
|
|
940
|
+
if (receiverResolved) {
|
|
941
|
+
const interfaceDefs = receiverResolved.candidates.filter(d => d.type === 'Interface' || d.type === 'Trait');
|
|
942
|
+
if (interfaceDefs.length > 0) {
|
|
943
|
+
for (const ifaceDef of interfaceDefs) {
|
|
944
|
+
// Strategy 1: Class-based — find IMPLEMENTS edges
|
|
945
|
+
const implRows = graph.relationships.filter(r => r.type === 'IMPLEMENTS' && r.targetId === toNodeId(ifaceDef.nodeId));
|
|
946
|
+
for (const implRel of implRows) {
|
|
947
|
+
const methodEdges = graph.relationships.filter(r => r.type === 'HAS_METHOD' && r.sourceId === implRel.sourceId);
|
|
948
|
+
for (const methodEdge of methodEdges) {
|
|
949
|
+
const methodNode = graph.getNode(methodEdge.targetId);
|
|
950
|
+
if (methodNode && methodNode.properties.name === effectiveCall.calledName) {
|
|
951
|
+
resolved = { nodeId: methodEdge.targetId, confidence: 0.85, reason: 'interface-dispatch' };
|
|
952
|
+
break;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
if (resolved)
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
if (resolved)
|
|
959
|
+
break;
|
|
960
|
+
// Strategy 2: Factory/closure pattern — find functions returning this interface
|
|
961
|
+
// e.g. createEventBus(): EventBus → the closure has emit/subscribe as inner functions
|
|
962
|
+
// Look for the method name in files that import the interface's file
|
|
963
|
+
if (!resolved) {
|
|
964
|
+
const methodName = effectiveCall.calledName;
|
|
965
|
+
const ifaceFile = ifaceDef.filePath;
|
|
966
|
+
// Check: which files import the interface's file?
|
|
967
|
+
const importingFiles = [];
|
|
968
|
+
for (const [file, importedFiles] of ctx.importMap) {
|
|
969
|
+
if (importedFiles.has(ifaceFile)) {
|
|
970
|
+
importingFiles.push(file);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
// Also check the interface's own file
|
|
974
|
+
importingFiles.push(ifaceFile);
|
|
975
|
+
for (const file of importingFiles) {
|
|
976
|
+
const method = ctx.symbols.lookupExactFull(file, methodName);
|
|
977
|
+
if (method && (method.type === 'Function' || method.type === 'Method')) {
|
|
978
|
+
resolved = { nodeId: method.nodeId, confidence: 0.75, reason: 'interface-factory-dispatch' };
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (resolved)
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
// RC-G: ORM chain recognition — for member chains like prisma.booking.findMany(),
|
|
990
|
+
// match the model name against entity classes (e.g. "Booking", "BookingEntity").
|
|
991
|
+
// Requires: (1) terminal method is a known ORM operation, (2) base receiver is a known ORM client name.
|
|
992
|
+
// Both gates prevent false positives from generic chains like filePath.includes().
|
|
993
|
+
if (!resolved && effectiveCall.callForm === 'member' && effectiveCall.receiverCallChain?.length) {
|
|
994
|
+
const ORM_METHODS = new Set([
|
|
995
|
+
'findMany', 'findFirst', 'findUnique', 'findUniqueOrThrow', 'findFirstOrThrow',
|
|
996
|
+
'create', 'createMany', 'update', 'updateMany', 'upsert', 'deleteMany',
|
|
997
|
+
'count', 'aggregate', 'groupBy', // Prisma
|
|
998
|
+
'findOne', 'findOneBy', 'findAndCount', 'save', 'softDelete',
|
|
999
|
+
'createQueryBuilder', // TypeORM
|
|
1000
|
+
'$queryRaw', '$executeRaw', // Prisma raw
|
|
1001
|
+
'bulkCreate', 'bulkUpdate', 'findAll', 'findByPk', // Sequelize
|
|
1002
|
+
]);
|
|
1003
|
+
// Known ORM client variable names — the base of the chain must match
|
|
1004
|
+
const ORM_CLIENTS = new Set([
|
|
1005
|
+
'prisma', 'db', 'knex', 'sequelize', 'typeorm', 'orm',
|
|
1006
|
+
'repo', 'repository', 'em', 'entityManager', 'dataSource',
|
|
1007
|
+
'connection', 'pool', 'client',
|
|
1008
|
+
]);
|
|
1009
|
+
// Extract the base receiver from the chain (the outermost object)
|
|
1010
|
+
// For prisma.booking.findMany: receiverCallChain=['booking'], receiverName might be undefined
|
|
1011
|
+
// but the chain's base should be 'prisma'
|
|
1012
|
+
const chainBase = effectiveCall.receiverCallChain[0];
|
|
1013
|
+
const baseReceiver = effectiveCall.receiverName?.toLowerCase();
|
|
1014
|
+
const isOrmChain = (baseReceiver && ORM_CLIENTS.has(baseReceiver))
|
|
1015
|
+
|| (chainBase && ORM_CLIENTS.has(chainBase.toLowerCase()));
|
|
1016
|
+
if (isOrmChain && ORM_METHODS.has(effectiveCall.calledName)) {
|
|
1017
|
+
// The model name is the intermediate chain element (e.g. "booking" from prisma.booking.findMany)
|
|
1018
|
+
const modelName = effectiveCall.receiverCallChain.length > 0
|
|
1019
|
+
? effectiveCall.receiverCallChain[effectiveCall.receiverCallChain.length - 1]
|
|
1020
|
+
: effectiveCall.receiverName;
|
|
1021
|
+
if (modelName && modelName.length > 2) {
|
|
1022
|
+
const capitalized = modelName.charAt(0).toUpperCase() + modelName.slice(1);
|
|
1023
|
+
const allEntityCandidates = ctx.symbols.lookupFuzzy(capitalized)
|
|
1024
|
+
.filter(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct');
|
|
1025
|
+
// Prefer entities sharing the closest common ancestor directory
|
|
1026
|
+
// e.g. cross-boundary/server/src/services/ calling → cross-boundary/server/src/models/ (same project)
|
|
1027
|
+
// but NOT → lang-resolution/ruby-calls/ (different project)
|
|
1028
|
+
const callerParts = effectiveCall.filePath.split('/');
|
|
1029
|
+
const entityCandidates = allEntityCandidates.filter(d => {
|
|
1030
|
+
const entityParts = d.filePath.split('/');
|
|
1031
|
+
// Require at least 4 shared path segments — no fallback to global.
|
|
1032
|
+
// A missing match is better than a false cross-project match.
|
|
1033
|
+
let shared = 0;
|
|
1034
|
+
for (let i = 0; i < Math.min(callerParts.length, entityParts.length); i++) {
|
|
1035
|
+
if (callerParts[i] === entityParts[i])
|
|
1036
|
+
shared++;
|
|
1037
|
+
else
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
return shared >= 4;
|
|
1041
|
+
});
|
|
1042
|
+
if (entityCandidates.length === 1) {
|
|
1043
|
+
resolved = {
|
|
1044
|
+
nodeId: entityCandidates[0].nodeId,
|
|
1045
|
+
confidence: 0.7,
|
|
1046
|
+
reason: 'orm-entity-match',
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
else if (entityCandidates.length === 0) {
|
|
1050
|
+
const entitySuffix = ctx.symbols.lookupFuzzy(capitalized + 'Entity')
|
|
1051
|
+
.filter(d => d.type === 'Class');
|
|
1052
|
+
if (entitySuffix.length === 1) {
|
|
1053
|
+
resolved = {
|
|
1054
|
+
nodeId: entitySuffix[0].nodeId,
|
|
1055
|
+
confidence: 0.7,
|
|
1056
|
+
reason: 'orm-entity-match',
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
762
1063
|
if (!resolved)
|
|
763
1064
|
continue;
|
|
764
|
-
|
|
765
|
-
|
|
1065
|
+
// Suppress self-referencing edges from unresolved method chains
|
|
1066
|
+
// (e.g. db('table').select().where() falsely resolving chain methods to the enclosing function)
|
|
1067
|
+
// Preserve legitimate recursion: only suppress member calls with no resolved receiver type
|
|
1068
|
+
const sourceNodeId = toNodeId(effectiveCall.sourceId);
|
|
1069
|
+
const targetNodeId = toNodeId(resolved.nodeId);
|
|
1070
|
+
if (sourceNodeId === targetNodeId && effectiveCall.callForm === 'member' && !effectiveCall.receiverTypeName) {
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
const relId = toEdgeId(`CALLS:${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
|
|
1074
|
+
const rel = {
|
|
766
1075
|
id: relId,
|
|
767
|
-
sourceId:
|
|
768
|
-
targetId:
|
|
1076
|
+
sourceId: sourceNodeId,
|
|
1077
|
+
targetId: targetNodeId,
|
|
769
1078
|
type: 'CALLS',
|
|
770
1079
|
confidence: resolved.confidence,
|
|
771
1080
|
reason: resolved.reason,
|
|
772
|
-
|
|
773
|
-
|
|
1081
|
+
};
|
|
1082
|
+
if (effectiveCall.callLine !== undefined)
|
|
1083
|
+
rel.callLine = effectiveCall.callLine;
|
|
1084
|
+
graph.addRelationship(rel);
|
|
774
1085
|
}
|
|
775
1086
|
ctx.clearCache();
|
|
776
1087
|
}
|
|
@@ -780,6 +1091,8 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
|
|
|
780
1091
|
export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, onProgress) => {
|
|
781
1092
|
for (let i = 0; i < extractedRoutes.length; i++) {
|
|
782
1093
|
const route = extractedRoutes[i];
|
|
1094
|
+
if (!route)
|
|
1095
|
+
continue;
|
|
783
1096
|
if (i % 50 === 0) {
|
|
784
1097
|
onProgress?.(i, extractedRoutes.length);
|
|
785
1098
|
await yieldToEventLoop();
|
|
@@ -792,13 +1105,16 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, on
|
|
|
792
1105
|
if (controllerResolved.tier === 'global' && controllerResolved.candidates.length > 1)
|
|
793
1106
|
continue;
|
|
794
1107
|
const controllerDef = controllerResolved.candidates[0];
|
|
1108
|
+
if (!controllerDef)
|
|
1109
|
+
continue;
|
|
795
1110
|
const confidence = TIER_CONFIDENCE[controllerResolved.tier];
|
|
796
1111
|
const methodResolved = ctx.resolve(route.methodName, controllerDef.filePath);
|
|
797
|
-
const
|
|
1112
|
+
const firstMethodCandidate = methodResolved?.tier === 'same-file' ? methodResolved.candidates[0] : undefined;
|
|
1113
|
+
const methodId = firstMethodCandidate?.nodeId;
|
|
798
1114
|
const sourceId = generateId('File', route.filePath);
|
|
799
1115
|
if (!methodId) {
|
|
800
1116
|
const guessedId = generateId('Method', `${controllerDef.filePath}:${route.methodName}`);
|
|
801
|
-
const relId =
|
|
1117
|
+
const relId = toEdgeId(`CALLS:${sourceId}:route->${guessedId}`);
|
|
802
1118
|
graph.addRelationship({
|
|
803
1119
|
id: relId,
|
|
804
1120
|
sourceId,
|
|
@@ -809,11 +1125,11 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, on
|
|
|
809
1125
|
});
|
|
810
1126
|
continue;
|
|
811
1127
|
}
|
|
812
|
-
const relId =
|
|
1128
|
+
const relId = toEdgeId(`CALLS:${sourceId}:route->${methodId}`);
|
|
813
1129
|
graph.addRelationship({
|
|
814
1130
|
id: relId,
|
|
815
1131
|
sourceId,
|
|
816
|
-
targetId: methodId,
|
|
1132
|
+
targetId: toNodeId(methodId),
|
|
817
1133
|
type: 'CALLS',
|
|
818
1134
|
confidence,
|
|
819
1135
|
reason: 'laravel-route',
|
|
@@ -868,9 +1184,9 @@ export const createDependsOnEdges = async (graph, ctx) => {
|
|
|
868
1184
|
continue;
|
|
869
1185
|
seen.add(edgeKey);
|
|
870
1186
|
graph.addRelationship({
|
|
871
|
-
id:
|
|
872
|
-
sourceId: symDef.ownerId,
|
|
873
|
-
targetId: target.nodeId,
|
|
1187
|
+
id: toEdgeId(`DEPENDS_ON:${edgeKey}`),
|
|
1188
|
+
sourceId: toNodeId(symDef.ownerId),
|
|
1189
|
+
targetId: toNodeId(target.nodeId),
|
|
874
1190
|
type: 'DEPENDS_ON',
|
|
875
1191
|
confidence: TIER_CONFIDENCE[resolved.tier],
|
|
876
1192
|
reason: 'constructor-injection',
|
|
@@ -895,14 +1211,16 @@ export const createProvidesEdges = async (graph, ctx) => {
|
|
|
895
1211
|
const candidates = [];
|
|
896
1212
|
graph.forEachNode(n => {
|
|
897
1213
|
if ((n.label === 'Method' || n.label === 'Function') && n.properties.returnType) {
|
|
898
|
-
|
|
1214
|
+
const entry = {
|
|
899
1215
|
id: n.id,
|
|
900
1216
|
filePath: n.properties.filePath,
|
|
901
1217
|
returnType: n.properties.returnType,
|
|
902
1218
|
startLine: n.properties.startLine ?? 0,
|
|
903
1219
|
endLine: n.properties.endLine ?? 0,
|
|
904
|
-
|
|
905
|
-
|
|
1220
|
+
};
|
|
1221
|
+
if (n.properties.astFrameworkReason !== undefined)
|
|
1222
|
+
entry.frameworkReason = n.properties.astFrameworkReason;
|
|
1223
|
+
candidates.push(entry);
|
|
906
1224
|
}
|
|
907
1225
|
});
|
|
908
1226
|
for (const node of candidates) {
|
|
@@ -924,9 +1242,9 @@ export const createProvidesEdges = async (graph, ctx) => {
|
|
|
924
1242
|
if (bodyLength > 20 && !isDIAnnotated)
|
|
925
1243
|
continue;
|
|
926
1244
|
graph.addRelationship({
|
|
927
|
-
id:
|
|
1245
|
+
id: toEdgeId(`PROVIDES:${node.id}->${interfaceTarget.nodeId}`),
|
|
928
1246
|
sourceId: node.id,
|
|
929
|
-
targetId: interfaceTarget.nodeId,
|
|
1247
|
+
targetId: toNodeId(interfaceTarget.nodeId),
|
|
930
1248
|
type: 'PROVIDES',
|
|
931
1249
|
confidence: TIER_CONFIDENCE[resolved.tier] * 0.9,
|
|
932
1250
|
reason: 'factory-return-type',
|
|
@@ -935,3 +1253,73 @@ export const createProvidesEdges = async (graph, ctx) => {
|
|
|
935
1253
|
}
|
|
936
1254
|
return edgesCreated;
|
|
937
1255
|
};
|
|
1256
|
+
/**
|
|
1257
|
+
* Post-pass: connect interface method DECLARATIONS to their IMPLEMENTATIONS.
|
|
1258
|
+
*
|
|
1259
|
+
* For each interface Method node (e.g. EventBus.emit), find the matching
|
|
1260
|
+
* implementation Function in files that import the interface definition.
|
|
1261
|
+
* Creates CALLS edges: InterfaceMethod → ImplementationFunction.
|
|
1262
|
+
*
|
|
1263
|
+
* This makes the call chain traversable:
|
|
1264
|
+
* register() →[tsgo]→ EventBus.emit (Method) →[dispatch]→ emit (Function in event-bus.ts)
|
|
1265
|
+
*/
|
|
1266
|
+
export const resolveInterfaceDispatches = async (graph, ctx) => {
|
|
1267
|
+
let edgesCreated = 0;
|
|
1268
|
+
const seen = new Set();
|
|
1269
|
+
// Step 1: Find all interface/trait Method nodes (via HAS_METHOD edges)
|
|
1270
|
+
const ifaceMethods = [];
|
|
1271
|
+
graph.forEachRelationship(rel => {
|
|
1272
|
+
if (rel.type !== 'HAS_METHOD')
|
|
1273
|
+
return;
|
|
1274
|
+
const parentNode = graph.getNode(rel.sourceId);
|
|
1275
|
+
if (!parentNode || (parentNode.label !== 'Interface' && parentNode.label !== 'Trait'))
|
|
1276
|
+
return;
|
|
1277
|
+
const methodNode = graph.getNode(rel.targetId);
|
|
1278
|
+
if (!methodNode)
|
|
1279
|
+
return;
|
|
1280
|
+
ifaceMethods.push({
|
|
1281
|
+
methodId: rel.targetId,
|
|
1282
|
+
methodName: methodNode.properties.name,
|
|
1283
|
+
ifaceFile: parentNode.properties.filePath,
|
|
1284
|
+
});
|
|
1285
|
+
});
|
|
1286
|
+
// Step 2: For each interface method, find implementation in files that import the interface
|
|
1287
|
+
for (const ifaceMethod of ifaceMethods) {
|
|
1288
|
+
const implFiles = [];
|
|
1289
|
+
const ifaceFileBase = ifaceMethod.ifaceFile.replace(/\.[^.]+$/, '');
|
|
1290
|
+
for (const [file, importedFiles] of ctx.importMap) {
|
|
1291
|
+
// Check exact match OR suffix match (import paths may differ in extension)
|
|
1292
|
+
let found = importedFiles.has(ifaceMethod.ifaceFile);
|
|
1293
|
+
if (!found) {
|
|
1294
|
+
for (const imp of importedFiles) {
|
|
1295
|
+
if (imp.replace(/\.[^.]+$/, '') === ifaceFileBase || imp.endsWith(ifaceFileBase) || ifaceFileBase.endsWith(imp.replace(/\.[^.]+$/, ''))) {
|
|
1296
|
+
found = true;
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (found)
|
|
1302
|
+
implFiles.push(file);
|
|
1303
|
+
}
|
|
1304
|
+
for (const implFile of implFiles) {
|
|
1305
|
+
// Look for a Function with the same name in the implementation file
|
|
1306
|
+
const implSym = ctx.symbols.lookupExactFull(implFile, ifaceMethod.methodName);
|
|
1307
|
+
if (!implSym || (implSym.type !== 'Function' && implSym.type !== 'Method'))
|
|
1308
|
+
continue;
|
|
1309
|
+
const edgeKey = `${ifaceMethod.methodId}->${implSym.nodeId}`;
|
|
1310
|
+
if (seen.has(edgeKey))
|
|
1311
|
+
continue;
|
|
1312
|
+
seen.add(edgeKey);
|
|
1313
|
+
graph.addRelationship({
|
|
1314
|
+
id: toEdgeId(`CALLS:iface-dispatch:${edgeKey}`),
|
|
1315
|
+
sourceId: toNodeId(ifaceMethod.methodId),
|
|
1316
|
+
targetId: toNodeId(implSym.nodeId),
|
|
1317
|
+
type: 'CALLS',
|
|
1318
|
+
confidence: 0.80,
|
|
1319
|
+
reason: 'interface-dispatch',
|
|
1320
|
+
});
|
|
1321
|
+
edgesCreated++;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return edgesCreated;
|
|
1325
|
+
};
|