gitnexus 1.4.0 → 1.4.5
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 +19 -18
- package/dist/cli/analyze.js +37 -28
- package/dist/cli/augment.js +1 -1
- package/dist/cli/eval-server.d.ts +1 -1
- package/dist/cli/eval-server.js +1 -1
- package/dist/cli/index.js +1 -0
- package/dist/cli/mcp.js +1 -1
- package/dist/cli/setup.js +25 -13
- package/dist/cli/status.js +13 -4
- package/dist/cli/tool.d.ts +1 -1
- package/dist/cli/tool.js +2 -2
- package/dist/cli/wiki.js +2 -2
- package/dist/config/ignore-service.d.ts +25 -0
- package/dist/config/ignore-service.js +76 -0
- package/dist/config/supported-languages.d.ts +1 -0
- package/dist/config/supported-languages.js +1 -1
- package/dist/core/augmentation/engine.js +94 -67
- package/dist/core/embeddings/embedder.d.ts +1 -1
- package/dist/core/embeddings/embedder.js +1 -1
- package/dist/core/embeddings/embedding-pipeline.d.ts +3 -3
- package/dist/core/embeddings/embedding-pipeline.js +52 -25
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/ingestion/call-processor.d.ts +6 -7
- package/dist/core/ingestion/call-processor.js +490 -127
- package/dist/core/ingestion/call-routing.d.ts +53 -0
- package/dist/core/ingestion/call-routing.js +108 -0
- package/dist/core/ingestion/entry-point-scoring.js +13 -2
- package/dist/core/ingestion/export-detection.js +1 -0
- package/dist/core/ingestion/filesystem-walker.js +4 -3
- package/dist/core/ingestion/framework-detection.js +9 -0
- package/dist/core/ingestion/heritage-processor.d.ts +3 -4
- package/dist/core/ingestion/heritage-processor.js +40 -50
- package/dist/core/ingestion/import-processor.d.ts +3 -5
- package/dist/core/ingestion/import-processor.js +41 -10
- package/dist/core/ingestion/parsing-processor.d.ts +2 -1
- package/dist/core/ingestion/parsing-processor.js +41 -4
- package/dist/core/ingestion/pipeline.d.ts +5 -1
- package/dist/core/ingestion/pipeline.js +174 -121
- package/dist/core/ingestion/resolution-context.d.ts +53 -0
- package/dist/core/ingestion/resolution-context.js +132 -0
- package/dist/core/ingestion/resolvers/index.d.ts +2 -0
- package/dist/core/ingestion/resolvers/index.js +2 -0
- package/dist/core/ingestion/resolvers/python.d.ts +19 -0
- package/dist/core/ingestion/resolvers/python.js +52 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +12 -0
- package/dist/core/ingestion/resolvers/ruby.js +15 -0
- package/dist/core/ingestion/resolvers/standard.js +0 -22
- package/dist/core/ingestion/resolvers/utils.js +2 -0
- package/dist/core/ingestion/symbol-table.d.ts +3 -0
- package/dist/core/ingestion/symbol-table.js +1 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +3 -2
- package/dist/core/ingestion/tree-sitter-queries.js +53 -1
- package/dist/core/ingestion/type-env.d.ts +32 -10
- package/dist/core/ingestion/type-env.js +520 -47
- package/dist/core/ingestion/type-extractors/c-cpp.js +326 -1
- package/dist/core/ingestion/type-extractors/csharp.js +282 -2
- package/dist/core/ingestion/type-extractors/go.js +333 -2
- package/dist/core/ingestion/type-extractors/index.d.ts +3 -2
- package/dist/core/ingestion/type-extractors/index.js +3 -1
- package/dist/core/ingestion/type-extractors/jvm.js +537 -4
- package/dist/core/ingestion/type-extractors/php.js +387 -7
- package/dist/core/ingestion/type-extractors/python.js +356 -5
- package/dist/core/ingestion/type-extractors/ruby.d.ts +2 -0
- package/dist/core/ingestion/type-extractors/ruby.js +389 -0
- package/dist/core/ingestion/type-extractors/rust.js +399 -2
- package/dist/core/ingestion/type-extractors/shared.d.ts +116 -1
- package/dist/core/ingestion/type-extractors/shared.js +488 -14
- package/dist/core/ingestion/type-extractors/swift.js +95 -1
- package/dist/core/ingestion/type-extractors/types.d.ts +81 -0
- package/dist/core/ingestion/type-extractors/typescript.js +436 -2
- package/dist/core/ingestion/utils.d.ts +33 -2
- package/dist/core/ingestion/utils.js +399 -27
- package/dist/core/ingestion/workers/parse-worker.d.ts +18 -1
- package/dist/core/ingestion/workers/parse-worker.js +169 -19
- package/dist/core/{kuzu → lbug}/csv-generator.d.ts +1 -1
- package/dist/core/{kuzu → lbug}/csv-generator.js +1 -1
- package/dist/core/{kuzu/kuzu-adapter.d.ts → lbug/lbug-adapter.d.ts} +19 -19
- package/dist/core/{kuzu/kuzu-adapter.js → lbug/lbug-adapter.js} +70 -65
- package/dist/core/{kuzu → lbug}/schema.d.ts +1 -1
- package/dist/core/{kuzu → lbug}/schema.js +1 -1
- package/dist/core/search/bm25-index.d.ts +4 -4
- package/dist/core/search/bm25-index.js +10 -10
- package/dist/core/search/hybrid-search.d.ts +2 -2
- package/dist/core/search/hybrid-search.js +6 -6
- package/dist/core/tree-sitter/parser-loader.js +9 -2
- package/dist/core/wiki/generator.d.ts +2 -2
- package/dist/core/wiki/generator.js +4 -4
- package/dist/core/wiki/graph-queries.d.ts +4 -4
- package/dist/core/wiki/graph-queries.js +7 -7
- package/dist/mcp/core/{kuzu-adapter.d.ts → lbug-adapter.d.ts} +7 -7
- package/dist/mcp/core/{kuzu-adapter.js → lbug-adapter.js} +72 -43
- package/dist/mcp/local/local-backend.d.ts +6 -6
- package/dist/mcp/local/local-backend.js +25 -18
- package/dist/server/api.js +12 -12
- package/dist/server/mcp-http.d.ts +1 -1
- package/dist/server/mcp-http.js +1 -1
- package/dist/storage/repo-manager.d.ts +20 -2
- package/dist/storage/repo-manager.js +55 -1
- package/dist/types/pipeline.d.ts +1 -1
- package/package.json +5 -3
- package/dist/core/ingestion/symbol-resolver.d.ts +0 -32
- package/dist/core/ingestion/symbol-resolver.js +0 -83
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { isFileInPackageDir } from './import-processor.js';
|
|
2
|
-
import { resolveSymbolInternal } from './symbol-resolver.js';
|
|
3
|
-
import { walkBindingChain } from './named-binding-extraction.js';
|
|
4
1
|
import Parser from 'tree-sitter';
|
|
2
|
+
import { TIER_CONFIDENCE } from './resolution-context.js';
|
|
5
3
|
import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
|
|
6
4
|
import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
|
|
7
5
|
import { generateId } from '../../lib/utils.js';
|
|
8
|
-
import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, } from './utils.js';
|
|
9
|
-
import { buildTypeEnv
|
|
6
|
+
import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, CALL_EXPRESSION_TYPES, extractCallChain, } from './utils.js';
|
|
7
|
+
import { buildTypeEnv } from './type-env.js';
|
|
10
8
|
import { getTreeSitterBufferSize } from './constants.js';
|
|
9
|
+
import { callRouters } from './call-routing.js';
|
|
11
10
|
/**
|
|
12
11
|
* Walk up the AST from a node to find the enclosing function/method.
|
|
13
12
|
* Returns null if the call is at module/file level (top-level code).
|
|
14
13
|
*/
|
|
15
|
-
const findEnclosingFunction = (node, filePath,
|
|
14
|
+
const findEnclosingFunction = (node, filePath, ctx) => {
|
|
16
15
|
let current = node.parent;
|
|
17
16
|
while (current) {
|
|
18
17
|
if (FUNCTION_NODE_TYPES.has(current.type)) {
|
|
19
18
|
const { funcName, label } = extractFunctionName(current);
|
|
20
19
|
if (funcName) {
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
return nodeId;
|
|
20
|
+
const resolved = ctx.resolve(funcName, filePath);
|
|
21
|
+
if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
|
|
22
|
+
return resolved.candidates[0].nodeId;
|
|
23
|
+
}
|
|
24
24
|
return generateId(label, `${filePath}:${funcName}`);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -28,8 +28,58 @@ const findEnclosingFunction = (node, filePath, symbolTable) => {
|
|
|
28
28
|
}
|
|
29
29
|
return null;
|
|
30
30
|
};
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Verify constructor bindings against SymbolTable and infer receiver types.
|
|
33
|
+
* Shared between sequential (processCalls) and worker (processCallsFromExtracted) paths.
|
|
34
|
+
*/
|
|
35
|
+
const verifyConstructorBindings = (bindings, filePath, ctx, graph) => {
|
|
36
|
+
const verified = new Map();
|
|
37
|
+
for (const { scope, varName, calleeName, receiverClassName } of bindings) {
|
|
38
|
+
const tiered = ctx.resolve(calleeName, filePath);
|
|
39
|
+
const isClass = tiered?.candidates.some(def => def.type === 'Class') ?? false;
|
|
40
|
+
if (isClass) {
|
|
41
|
+
verified.set(receiverKey(scope, varName), calleeName);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
let callableDefs = tiered?.candidates.filter(d => d.type === 'Function' || d.type === 'Method');
|
|
45
|
+
// When receiver class is known (e.g. $this->method() in PHP), narrow
|
|
46
|
+
// candidates to methods owned by that class to avoid false disambiguation failures.
|
|
47
|
+
if (callableDefs && callableDefs.length > 1 && receiverClassName) {
|
|
48
|
+
if (graph) {
|
|
49
|
+
// Worker path: use graph.getNode (fast, already in-memory)
|
|
50
|
+
const narrowed = callableDefs.filter(d => {
|
|
51
|
+
if (!d.ownerId)
|
|
52
|
+
return false;
|
|
53
|
+
const owner = graph.getNode(d.ownerId);
|
|
54
|
+
return owner?.properties.name === receiverClassName;
|
|
55
|
+
});
|
|
56
|
+
if (narrowed.length > 0)
|
|
57
|
+
callableDefs = narrowed;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Sequential path: use ctx.resolve (no graph available)
|
|
61
|
+
const classResolved = ctx.resolve(receiverClassName, filePath);
|
|
62
|
+
if (classResolved && classResolved.candidates.length > 0) {
|
|
63
|
+
const classNodeIds = new Set(classResolved.candidates.map(c => c.nodeId));
|
|
64
|
+
const narrowed = callableDefs.filter(d => d.ownerId && classNodeIds.has(d.ownerId));
|
|
65
|
+
if (narrowed.length > 0)
|
|
66
|
+
callableDefs = narrowed;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (callableDefs && callableDefs.length === 1 && callableDefs[0].returnType) {
|
|
71
|
+
const typeName = extractReturnTypeName(callableDefs[0].returnType);
|
|
72
|
+
if (typeName) {
|
|
73
|
+
verified.set(receiverKey(scope, varName), typeName);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return verified;
|
|
79
|
+
};
|
|
80
|
+
export const processCalls = async (graph, files, astCache, ctx, onProgress) => {
|
|
32
81
|
const parser = await loadParser();
|
|
82
|
+
const collectedHeritage = [];
|
|
33
83
|
const logSkipped = isVerboseIngestionEnabled();
|
|
34
84
|
const skippedByLang = logSkipped ? new Map() : null;
|
|
35
85
|
for (let i = 0; i < files.length; i++) {
|
|
@@ -37,7 +87,6 @@ export const processCalls = async (graph, files, astCache, symbolTable, importMa
|
|
|
37
87
|
onProgress?.(i + 1, files.length);
|
|
38
88
|
if (i % 20 === 0)
|
|
39
89
|
await yieldToEventLoop();
|
|
40
|
-
// 1. Check language support first
|
|
41
90
|
const language = getLanguageFromFilename(file.path);
|
|
42
91
|
if (!language)
|
|
43
92
|
continue;
|
|
@@ -50,23 +99,15 @@ export const processCalls = async (graph, files, astCache, symbolTable, importMa
|
|
|
50
99
|
const queryStr = LANGUAGE_QUERIES[language];
|
|
51
100
|
if (!queryStr)
|
|
52
101
|
continue;
|
|
53
|
-
// 2. ALWAYS load the language before querying (parser is stateful)
|
|
54
102
|
await loadLanguage(language, file.path);
|
|
55
|
-
// 3. Get AST (Try Cache First)
|
|
56
103
|
let tree = astCache.get(file.path);
|
|
57
|
-
let wasReparsed = false;
|
|
58
104
|
if (!tree) {
|
|
59
|
-
// Cache Miss: Re-parse
|
|
60
|
-
// Use larger bufferSize for files > 32KB
|
|
61
105
|
try {
|
|
62
106
|
tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
|
|
63
107
|
}
|
|
64
108
|
catch (parseError) {
|
|
65
|
-
// Skip files that can't be parsed
|
|
66
109
|
continue;
|
|
67
110
|
}
|
|
68
|
-
wasReparsed = true;
|
|
69
|
-
// Cache re-parsed tree so heritage phase gets hits
|
|
70
111
|
astCache.set(file.path, tree);
|
|
71
112
|
}
|
|
72
113
|
let query;
|
|
@@ -80,39 +121,130 @@ export const processCalls = async (graph, files, astCache, symbolTable, importMa
|
|
|
80
121
|
console.warn(`Query error for ${file.path}:`, queryError);
|
|
81
122
|
continue;
|
|
82
123
|
}
|
|
83
|
-
// Build per-file TypeEnv for receiver resolution
|
|
84
124
|
const lang = getLanguageFromFilename(file.path);
|
|
85
|
-
const typeEnv = lang ? buildTypeEnv(tree, lang) :
|
|
86
|
-
|
|
125
|
+
const typeEnv = lang ? buildTypeEnv(tree, lang, ctx.symbols) : null;
|
|
126
|
+
const callRouter = callRouters[language];
|
|
127
|
+
const verifiedReceivers = typeEnv && typeEnv.constructorBindings.length > 0
|
|
128
|
+
? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx)
|
|
129
|
+
: new Map();
|
|
130
|
+
ctx.enableCache(file.path);
|
|
87
131
|
matches.forEach(match => {
|
|
88
132
|
const captureMap = {};
|
|
89
133
|
match.captures.forEach(c => captureMap[c.name] = c.node);
|
|
90
|
-
// Only process @call captures
|
|
91
134
|
if (!captureMap['call'])
|
|
92
135
|
return;
|
|
93
136
|
const nameNode = captureMap['call.name'];
|
|
94
137
|
if (!nameNode)
|
|
95
138
|
return;
|
|
96
139
|
const calledName = nameNode.text;
|
|
97
|
-
|
|
140
|
+
const routed = callRouter(calledName, captureMap['call']);
|
|
141
|
+
if (routed) {
|
|
142
|
+
switch (routed.kind) {
|
|
143
|
+
case 'skip':
|
|
144
|
+
case 'import':
|
|
145
|
+
return;
|
|
146
|
+
case 'heritage':
|
|
147
|
+
for (const item of routed.items) {
|
|
148
|
+
collectedHeritage.push({
|
|
149
|
+
filePath: file.path,
|
|
150
|
+
className: item.enclosingClass,
|
|
151
|
+
parentName: item.mixinName,
|
|
152
|
+
kind: item.heritageKind,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
case 'properties': {
|
|
157
|
+
const fileId = generateId('File', file.path);
|
|
158
|
+
const propEnclosingClassId = findEnclosingClassId(captureMap['call'], file.path);
|
|
159
|
+
for (const item of routed.items) {
|
|
160
|
+
const nodeId = generateId('Property', `${file.path}:${item.propName}`);
|
|
161
|
+
graph.addNode({
|
|
162
|
+
id: nodeId,
|
|
163
|
+
label: 'Property',
|
|
164
|
+
properties: {
|
|
165
|
+
name: item.propName, filePath: file.path,
|
|
166
|
+
startLine: item.startLine, endLine: item.endLine,
|
|
167
|
+
language, isExported: true,
|
|
168
|
+
description: item.accessorType,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
ctx.symbols.add(file.path, item.propName, nodeId, 'Property', propEnclosingClassId ? { ownerId: propEnclosingClassId } : undefined);
|
|
172
|
+
const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
|
|
173
|
+
graph.addRelationship({
|
|
174
|
+
id: relId, sourceId: fileId, targetId: nodeId,
|
|
175
|
+
type: 'DEFINES', confidence: 1.0, reason: '',
|
|
176
|
+
});
|
|
177
|
+
if (propEnclosingClassId) {
|
|
178
|
+
graph.addRelationship({
|
|
179
|
+
id: generateId('HAS_METHOD', `${propEnclosingClassId}->${nodeId}`),
|
|
180
|
+
sourceId: propEnclosingClassId, targetId: nodeId,
|
|
181
|
+
type: 'HAS_METHOD', confidence: 1.0, reason: '',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
case 'call':
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
98
191
|
if (isBuiltInOrNoise(calledName))
|
|
99
192
|
return;
|
|
100
193
|
const callNode = captureMap['call'];
|
|
101
194
|
const callForm = inferCallForm(callNode, nameNode);
|
|
102
195
|
const receiverName = callForm === 'member' ? extractReceiverName(nameNode) : undefined;
|
|
103
|
-
|
|
104
|
-
//
|
|
196
|
+
let receiverTypeName = receiverName && typeEnv ? typeEnv.lookup(receiverName, callNode) : undefined;
|
|
197
|
+
// Fall back to verified constructor bindings for return type inference
|
|
198
|
+
if (!receiverTypeName && receiverName && verifiedReceivers.size > 0) {
|
|
199
|
+
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
|
200
|
+
const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
|
|
201
|
+
receiverTypeName = lookupReceiverType(verifiedReceivers, funcName, receiverName);
|
|
202
|
+
}
|
|
203
|
+
// Fall back to class-as-receiver for static method calls (e.g. UserService.find_user()).
|
|
204
|
+
// When the receiver name is not a variable in TypeEnv but resolves to a Class/Struct/Interface
|
|
205
|
+
// through the standard tiered resolution, use it directly as the receiver type.
|
|
206
|
+
if (!receiverTypeName && receiverName && callForm === 'member') {
|
|
207
|
+
const typeResolved = ctx.resolve(receiverName, file.path);
|
|
208
|
+
if (typeResolved && typeResolved.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
|
|
209
|
+
receiverTypeName = receiverName;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Fall back to chained call resolution when the receiver is a call expression
|
|
213
|
+
// (e.g. svc.getUser().save() — receiver of save() is getUser(), not a simple identifier).
|
|
214
|
+
if (callForm === 'member' && !receiverTypeName && !receiverName) {
|
|
215
|
+
const receiverNode = extractReceiverNode(nameNode);
|
|
216
|
+
if (receiverNode && CALL_EXPRESSION_TYPES.has(receiverNode.type)) {
|
|
217
|
+
const extracted = extractCallChain(receiverNode);
|
|
218
|
+
if (extracted) {
|
|
219
|
+
// Resolve the base receiver type if possible
|
|
220
|
+
let baseType = extracted.baseReceiverName && typeEnv
|
|
221
|
+
? typeEnv.lookup(extracted.baseReceiverName, callNode)
|
|
222
|
+
: undefined;
|
|
223
|
+
if (!baseType && extracted.baseReceiverName && verifiedReceivers.size > 0) {
|
|
224
|
+
const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
|
|
225
|
+
const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
|
|
226
|
+
baseType = lookupReceiverType(verifiedReceivers, funcName, extracted.baseReceiverName);
|
|
227
|
+
}
|
|
228
|
+
// Class-as-receiver for chain base (e.g. UserService.find_user().save())
|
|
229
|
+
if (!baseType && extracted.baseReceiverName) {
|
|
230
|
+
const cr = ctx.resolve(extracted.baseReceiverName, file.path);
|
|
231
|
+
if (cr?.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
|
|
232
|
+
baseType = extracted.baseReceiverName;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
receiverTypeName = resolveChainedReceiver(extracted.chain, baseType, file.path, ctx);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
105
239
|
const resolved = resolveCallTarget({
|
|
106
240
|
calledName,
|
|
107
241
|
argCount: countCallArguments(callNode),
|
|
108
242
|
callForm,
|
|
109
243
|
receiverTypeName,
|
|
110
|
-
}, file.path,
|
|
244
|
+
}, file.path, ctx);
|
|
111
245
|
if (!resolved)
|
|
112
246
|
return;
|
|
113
|
-
|
|
114
|
-
const enclosingFuncId = findEnclosingFunction(callNode, file.path, symbolTable);
|
|
115
|
-
// Use enclosing function as source, fallback to file for top-level calls
|
|
247
|
+
const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
|
|
116
248
|
const sourceId = enclosingFuncId || generateId('File', file.path);
|
|
117
249
|
const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
|
|
118
250
|
graph.addRelationship({
|
|
@@ -124,13 +256,14 @@ export const processCalls = async (graph, files, astCache, symbolTable, importMa
|
|
|
124
256
|
reason: resolved.reason,
|
|
125
257
|
});
|
|
126
258
|
});
|
|
127
|
-
|
|
259
|
+
ctx.clearCache();
|
|
128
260
|
}
|
|
129
261
|
if (skippedByLang && skippedByLang.size > 0) {
|
|
130
262
|
for (const [lang, count] of skippedByLang.entries()) {
|
|
131
263
|
console.warn(`[ingestion] Skipped ${count} ${lang} file(s) in call processing — ${lang} parser not available.`);
|
|
132
264
|
}
|
|
133
265
|
}
|
|
266
|
+
return collectedHeritage;
|
|
134
267
|
};
|
|
135
268
|
const CALLABLE_SYMBOL_TYPES = new Set([
|
|
136
269
|
'Function',
|
|
@@ -139,54 +272,10 @@ const CALLABLE_SYMBOL_TYPES = new Set([
|
|
|
139
272
|
'Macro',
|
|
140
273
|
'Delegate',
|
|
141
274
|
]);
|
|
142
|
-
const collectTieredCandidates = (calledName, currentFile, symbolTable, importMap, packageMap, namedImportMap) => {
|
|
143
|
-
const allDefs = symbolTable.lookupFuzzy(calledName);
|
|
144
|
-
// Tier 1: Same-file — highest priority, prevents imports from shadowing local defs
|
|
145
|
-
// (matches resolveSymbolInternal which checks lookupExactFull before named bindings)
|
|
146
|
-
const localDefs = allDefs.filter(def => def.filePath === currentFile);
|
|
147
|
-
if (localDefs.length > 0) {
|
|
148
|
-
return { candidates: localDefs, tier: 'same-file' };
|
|
149
|
-
}
|
|
150
|
-
// Tier 2a-named: Check named bindings with re-export chain following.
|
|
151
|
-
// Aliased imports (import { User as U }) mean lookupFuzzy('U') returns
|
|
152
|
-
// empty but we can resolve via the exported name.
|
|
153
|
-
// Re-exports (export { User } from './base') are followed up to 5 hops.
|
|
154
|
-
if (namedImportMap) {
|
|
155
|
-
const chainResult = resolveNamedBindingChainForCandidates(calledName, currentFile, symbolTable, namedImportMap, allDefs);
|
|
156
|
-
if (chainResult)
|
|
157
|
-
return chainResult;
|
|
158
|
-
}
|
|
159
|
-
if (allDefs.length === 0)
|
|
160
|
-
return null;
|
|
161
|
-
const importedFiles = importMap.get(currentFile);
|
|
162
|
-
if (importedFiles) {
|
|
163
|
-
const importedDefs = allDefs.filter(def => importedFiles.has(def.filePath));
|
|
164
|
-
if (importedDefs.length > 0) {
|
|
165
|
-
return { candidates: importedDefs, tier: 'import-scoped' };
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const importedPackages = packageMap?.get(currentFile);
|
|
169
|
-
if (importedPackages) {
|
|
170
|
-
const packageDefs = allDefs.filter(def => {
|
|
171
|
-
for (const dirSuffix of importedPackages) {
|
|
172
|
-
if (isFileInPackageDir(def.filePath, dirSuffix))
|
|
173
|
-
return true;
|
|
174
|
-
}
|
|
175
|
-
return false;
|
|
176
|
-
});
|
|
177
|
-
if (packageDefs.length > 0) {
|
|
178
|
-
return { candidates: packageDefs, tier: 'import-scoped' };
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
// Tier 3: Global — pass all candidates through; filterCallableCandidates
|
|
182
|
-
// will narrow by kind/arity and resolveCallTarget only emits when exactly 1 remains.
|
|
183
|
-
return { candidates: allDefs, tier: 'unique-global' };
|
|
184
|
-
};
|
|
185
275
|
const CONSTRUCTOR_TARGET_TYPES = new Set(['Constructor', 'Class', 'Struct', 'Record']);
|
|
186
276
|
const filterCallableCandidates = (candidates, argCount, callForm) => {
|
|
187
277
|
let kindFiltered;
|
|
188
278
|
if (callForm === 'constructor') {
|
|
189
|
-
// For constructor calls, prefer Constructor > Class/Struct/Record > callable fallback
|
|
190
279
|
const constructors = candidates.filter(c => c.type === 'Constructor');
|
|
191
280
|
if (constructors.length > 0) {
|
|
192
281
|
kindFiltered = constructors;
|
|
@@ -208,42 +297,88 @@ const filterCallableCandidates = (candidates, argCount, callForm) => {
|
|
|
208
297
|
return kindFiltered;
|
|
209
298
|
return kindFiltered.filter(candidate => candidate.parameterCount === undefined || candidate.parameterCount === argCount);
|
|
210
299
|
};
|
|
211
|
-
const toResolveResult = (definition, tier) => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
300
|
+
const toResolveResult = (definition, tier) => ({
|
|
301
|
+
nodeId: definition.nodeId,
|
|
302
|
+
confidence: TIER_CONFIDENCE[tier],
|
|
303
|
+
reason: tier === 'same-file' ? 'same-file' : tier === 'import-scoped' ? 'import-resolved' : 'global',
|
|
304
|
+
});
|
|
305
|
+
/**
|
|
306
|
+
* Resolve a chain of intermediate method calls to find the receiver type for a
|
|
307
|
+
* final member call. Called when the receiver of a call is itself a call
|
|
308
|
+
* expression (e.g. `svc.getUser().save()`).
|
|
309
|
+
*
|
|
310
|
+
* @param chainNames Ordered list of method names from outermost to innermost
|
|
311
|
+
* intermediate call (e.g. ['getUser'] for `svc.getUser().save()`).
|
|
312
|
+
* @param baseReceiverTypeName The already-resolved type of the base receiver
|
|
313
|
+
* (e.g. 'UserService' for `svc`), or undefined.
|
|
314
|
+
* @param currentFile The file path for resolution context.
|
|
315
|
+
* @param ctx The resolution context for symbol lookup.
|
|
316
|
+
* @returns The type name of the final intermediate call's return type, or undefined
|
|
317
|
+
* if resolution fails at any step.
|
|
318
|
+
*/
|
|
319
|
+
function resolveChainedReceiver(chainNames, baseReceiverTypeName, currentFile, ctx) {
|
|
320
|
+
let currentType = baseReceiverTypeName;
|
|
321
|
+
for (const name of chainNames) {
|
|
322
|
+
const resolved = resolveCallTarget({ calledName: name, callForm: 'member', receiverTypeName: currentType }, currentFile, ctx);
|
|
323
|
+
if (!resolved)
|
|
324
|
+
return undefined;
|
|
325
|
+
const candidates = ctx.symbols.lookupFuzzy(name);
|
|
326
|
+
const symDef = candidates.find(c => c.nodeId === resolved.nodeId);
|
|
327
|
+
if (!symDef?.returnType)
|
|
328
|
+
return undefined;
|
|
329
|
+
const returnTypeName = extractReturnTypeName(symDef.returnType);
|
|
330
|
+
if (!returnTypeName)
|
|
331
|
+
return undefined;
|
|
332
|
+
currentType = returnTypeName;
|
|
217
333
|
}
|
|
218
|
-
return
|
|
219
|
-
}
|
|
334
|
+
return currentType;
|
|
335
|
+
}
|
|
220
336
|
/**
|
|
221
337
|
* Resolve a function call to its target node ID using priority strategy:
|
|
222
|
-
* A. Narrow candidates by scope tier (
|
|
338
|
+
* A. Narrow candidates by scope tier via ctx.resolve()
|
|
223
339
|
* B. Filter to callable symbol kinds (constructor-aware when callForm is set)
|
|
224
340
|
* C. Apply arity filtering when parameter metadata is available
|
|
225
341
|
* D. Apply receiver-type filtering for member calls with typed receivers
|
|
226
342
|
*
|
|
227
343
|
* If filtering still leaves multiple candidates, refuse to emit a CALLS edge.
|
|
228
344
|
*/
|
|
229
|
-
const resolveCallTarget = (call, currentFile,
|
|
230
|
-
const tiered =
|
|
345
|
+
const resolveCallTarget = (call, currentFile, ctx) => {
|
|
346
|
+
const tiered = ctx.resolve(call.calledName, currentFile);
|
|
231
347
|
if (!tiered)
|
|
232
348
|
return null;
|
|
233
349
|
const filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
|
|
234
350
|
// D. Receiver-type filtering: for member calls with a known receiver type,
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
351
|
+
// resolve the type through the same tiered import infrastructure, then
|
|
352
|
+
// filter method candidates to the type's defining file. Fall back to
|
|
353
|
+
// fuzzy ownerId matching only when file-based narrowing is inconclusive.
|
|
354
|
+
//
|
|
355
|
+
// Applied regardless of candidate count — the sole same-file candidate may
|
|
356
|
+
// belong to the wrong class (e.g. super.save() should hit the parent's save,
|
|
357
|
+
// not the child's own save method in the same file).
|
|
358
|
+
if (call.callForm === 'member' && call.receiverTypeName) {
|
|
359
|
+
// D1. Resolve the receiver type
|
|
360
|
+
const typeResolved = ctx.resolve(call.receiverTypeName, currentFile);
|
|
361
|
+
if (typeResolved && typeResolved.candidates.length > 0) {
|
|
362
|
+
const typeNodeIds = new Set(typeResolved.candidates.map(d => d.nodeId));
|
|
363
|
+
const typeFiles = new Set(typeResolved.candidates.map(d => d.filePath));
|
|
364
|
+
// D2. Widen candidates: same-file tier may miss the parent's method when
|
|
365
|
+
// it lives in another file. Query the symbol table directly for all
|
|
366
|
+
// global methods with this name, then apply arity/kind filtering.
|
|
367
|
+
const methodPool = filteredCandidates.length <= 1
|
|
368
|
+
? filterCallableCandidates(ctx.symbols.lookupFuzzy(call.calledName), call.argCount, call.callForm)
|
|
369
|
+
: filteredCandidates;
|
|
370
|
+
// D3. File-based: prefer candidates whose filePath matches the resolved type's file
|
|
371
|
+
const fileFiltered = methodPool.filter(c => typeFiles.has(c.filePath));
|
|
372
|
+
if (fileFiltered.length === 1) {
|
|
373
|
+
return toResolveResult(fileFiltered[0], tiered.tier);
|
|
374
|
+
}
|
|
375
|
+
// D4. ownerId fallback: narrow by ownerId matching the type's nodeId
|
|
376
|
+
const pool = fileFiltered.length > 0 ? fileFiltered : methodPool;
|
|
377
|
+
const ownerFiltered = pool.filter(c => c.ownerId && typeNodeIds.has(c.ownerId));
|
|
241
378
|
if (ownerFiltered.length === 1) {
|
|
242
379
|
return toResolveResult(ownerFiltered[0], tiered.tier);
|
|
243
380
|
}
|
|
244
|
-
|
|
245
|
-
// If still 2+, refuse (don't guess)
|
|
246
|
-
if (ownerFiltered.length > 1)
|
|
381
|
+
if (fileFiltered.length > 1 || ownerFiltered.length > 1)
|
|
247
382
|
return null;
|
|
248
383
|
}
|
|
249
384
|
}
|
|
@@ -251,13 +386,218 @@ const resolveCallTarget = (call, currentFile, symbolTable, importMap, packageMap
|
|
|
251
386
|
return null;
|
|
252
387
|
return toResolveResult(filteredCandidates[0], tiered.tier);
|
|
253
388
|
};
|
|
389
|
+
// ── Return type text helpers ─────────────────────────────────────────────
|
|
390
|
+
// extractSimpleTypeName works on AST nodes; this operates on raw return-type
|
|
391
|
+
// text already stored in SymbolDefinition (e.g. "User", "Promise<User>",
|
|
392
|
+
// "User | null", "*User"). Extracts the base user-defined type name.
|
|
393
|
+
/** Primitive / built-in types that should NOT produce a receiver binding. */
|
|
394
|
+
const PRIMITIVE_TYPES = new Set([
|
|
395
|
+
'string', 'number', 'boolean', 'void', 'int', 'float', 'double', 'long',
|
|
396
|
+
'short', 'byte', 'char', 'bool', 'str', 'i8', 'i16', 'i32', 'i64',
|
|
397
|
+
'u8', 'u16', 'u32', 'u64', 'f32', 'f64', 'usize', 'isize',
|
|
398
|
+
'undefined', 'null', 'None', 'nil',
|
|
399
|
+
]);
|
|
400
|
+
/**
|
|
401
|
+
* Extract a simple type name from raw return-type text.
|
|
402
|
+
* Handles common patterns:
|
|
403
|
+
* "User" → "User"
|
|
404
|
+
* "Promise<User>" → "User" (unwrap wrapper generics)
|
|
405
|
+
* "Option<User>" → "User"
|
|
406
|
+
* "Result<User, Error>" → "User" (first type arg)
|
|
407
|
+
* "User | null" → "User" (strip nullable union)
|
|
408
|
+
* "User?" → "User" (strip nullable suffix)
|
|
409
|
+
* "*User" → "User" (Go pointer)
|
|
410
|
+
* "&User" → "User" (Rust reference)
|
|
411
|
+
* Returns undefined for complex types or primitives.
|
|
412
|
+
*/
|
|
413
|
+
const WRAPPER_GENERICS = new Set([
|
|
414
|
+
'Promise', 'Observable', 'Future', 'CompletableFuture', 'Task', 'ValueTask', // async wrappers
|
|
415
|
+
'Option', 'Some', 'Optional', 'Maybe', // nullable wrappers
|
|
416
|
+
'Result', 'Either', // result wrappers
|
|
417
|
+
// Rust smart pointers (Deref to inner type)
|
|
418
|
+
'Rc', 'Arc', 'Weak', // pointer types
|
|
419
|
+
'MutexGuard', 'RwLockReadGuard', 'RwLockWriteGuard', // guard types
|
|
420
|
+
'Ref', 'RefMut', // RefCell guards
|
|
421
|
+
'Cow', // copy-on-write
|
|
422
|
+
// Containers (List, Array, Vec, Set, etc.) are intentionally excluded —
|
|
423
|
+
// methods are called on the container, not the element type.
|
|
424
|
+
// Non-wrapper generics return the base type (e.g., List) via the else branch.
|
|
425
|
+
]);
|
|
426
|
+
/**
|
|
427
|
+
* Extracts the first type argument from a comma-separated generic argument string,
|
|
428
|
+
* respecting nested angle brackets. For example:
|
|
429
|
+
* "Result<User, Error>" → "Result<User, Error>" (no top-level comma)
|
|
430
|
+
* "User, Error" → "User"
|
|
431
|
+
* "Map<K, V>, string" → "Map<K, V>"
|
|
432
|
+
*/
|
|
433
|
+
function extractFirstGenericArg(args) {
|
|
434
|
+
let depth = 0;
|
|
435
|
+
for (let i = 0; i < args.length; i++) {
|
|
436
|
+
if (args[i] === '<')
|
|
437
|
+
depth++;
|
|
438
|
+
else if (args[i] === '>')
|
|
439
|
+
depth--;
|
|
440
|
+
else if (args[i] === ',' && depth === 0)
|
|
441
|
+
return args.slice(0, i).trim();
|
|
442
|
+
}
|
|
443
|
+
return args.trim();
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Extract the first non-lifetime type argument from a generic argument string.
|
|
447
|
+
* Skips Rust lifetime parameters (e.g., `'a`, `'_`) to find the actual type.
|
|
448
|
+
* "'_, User" → "User"
|
|
449
|
+
* "'a, User" → "User"
|
|
450
|
+
* "User, Error" → "User" (no lifetime — delegates to extractFirstGenericArg)
|
|
451
|
+
*/
|
|
452
|
+
function extractFirstTypeArg(args) {
|
|
453
|
+
let remaining = args;
|
|
454
|
+
while (remaining) {
|
|
455
|
+
const first = extractFirstGenericArg(remaining);
|
|
456
|
+
if (!first.startsWith("'"))
|
|
457
|
+
return first;
|
|
458
|
+
// Skip past this lifetime arg + the comma separator
|
|
459
|
+
const commaIdx = remaining.indexOf(',', first.length);
|
|
460
|
+
if (commaIdx < 0)
|
|
461
|
+
return first; // only lifetimes — fall through
|
|
462
|
+
remaining = remaining.slice(commaIdx + 1).trim();
|
|
463
|
+
}
|
|
464
|
+
return args.trim();
|
|
465
|
+
}
|
|
466
|
+
const MAX_RETURN_TYPE_INPUT_LENGTH = 2048;
|
|
467
|
+
const MAX_RETURN_TYPE_LENGTH = 512;
|
|
468
|
+
export const extractReturnTypeName = (raw, depth = 0) => {
|
|
469
|
+
if (depth > 10)
|
|
470
|
+
return undefined;
|
|
471
|
+
if (raw.length > MAX_RETURN_TYPE_INPUT_LENGTH)
|
|
472
|
+
return undefined;
|
|
473
|
+
let text = raw.trim();
|
|
474
|
+
if (!text)
|
|
475
|
+
return undefined;
|
|
476
|
+
// Strip pointer/reference prefixes: *User, &User, &mut User
|
|
477
|
+
text = text.replace(/^[&*]+\s*(mut\s+)?/, '');
|
|
478
|
+
// Strip nullable suffix: User?
|
|
479
|
+
text = text.replace(/\?$/, '');
|
|
480
|
+
// Handle union types: "User | null" → "User"
|
|
481
|
+
if (text.includes('|')) {
|
|
482
|
+
const parts = text.split('|').map(p => p.trim()).filter(p => p !== 'null' && p !== 'undefined' && p !== 'void' && p !== 'None' && p !== 'nil');
|
|
483
|
+
if (parts.length === 1)
|
|
484
|
+
text = parts[0];
|
|
485
|
+
else
|
|
486
|
+
return undefined; // genuine union — too complex
|
|
487
|
+
}
|
|
488
|
+
// Handle generics: Promise<User> → unwrap if wrapper, else take base
|
|
489
|
+
const genericMatch = text.match(/^(\w+)\s*<(.+)>$/);
|
|
490
|
+
if (genericMatch) {
|
|
491
|
+
const [, base, args] = genericMatch;
|
|
492
|
+
if (WRAPPER_GENERICS.has(base)) {
|
|
493
|
+
// Take the first non-lifetime type argument, using bracket-balanced splitting
|
|
494
|
+
// so that nested generics like Result<User, Error> are not split at the inner
|
|
495
|
+
// comma. Lifetime parameters (Rust 'a, '_) are skipped.
|
|
496
|
+
const firstArg = extractFirstTypeArg(args);
|
|
497
|
+
return extractReturnTypeName(firstArg, depth + 1);
|
|
498
|
+
}
|
|
499
|
+
// Non-wrapper generic: return the base type (e.g., Map<K,V> → Map)
|
|
500
|
+
return PRIMITIVE_TYPES.has(base.toLowerCase()) ? undefined : base;
|
|
501
|
+
}
|
|
502
|
+
// Bare wrapper type without generic argument (e.g. Task, Promise, Option)
|
|
503
|
+
// should not produce a binding — these are meaningless without a type parameter
|
|
504
|
+
if (WRAPPER_GENERICS.has(text))
|
|
505
|
+
return undefined;
|
|
506
|
+
// Handle qualified names: models.User → User, Models::User → User, \App\Models\User → User
|
|
507
|
+
if (text.includes('::') || text.includes('.') || text.includes('\\')) {
|
|
508
|
+
text = text.split(/::|[.\\]/).pop();
|
|
509
|
+
}
|
|
510
|
+
// Final check: skip primitives
|
|
511
|
+
if (PRIMITIVE_TYPES.has(text) || PRIMITIVE_TYPES.has(text.toLowerCase()))
|
|
512
|
+
return undefined;
|
|
513
|
+
// Must start with uppercase (class/type convention) or be a valid identifier
|
|
514
|
+
if (!/^[A-Z_]\w*$/.test(text))
|
|
515
|
+
return undefined;
|
|
516
|
+
// If the final extracted type name is too long, reject it
|
|
517
|
+
if (text.length > MAX_RETURN_TYPE_LENGTH)
|
|
518
|
+
return undefined;
|
|
519
|
+
return text;
|
|
520
|
+
};
|
|
521
|
+
// ── Scope key helpers ────────────────────────────────────────────────────
|
|
522
|
+
// Scope keys use the format "funcName@startIndex" (produced by type-env.ts).
|
|
523
|
+
// Source IDs use "Label:filepath:funcName" (produced by parse-worker.ts).
|
|
524
|
+
// NUL (\0) is used as a composite-key separator because it cannot appear
|
|
525
|
+
// in source-code identifiers, preventing ambiguous concatenation.
|
|
526
|
+
//
|
|
527
|
+
// receiverKey stores the FULL scope (funcName@startIndex) to prevent
|
|
528
|
+
// collisions between overloaded methods with the same name in different
|
|
529
|
+
// classes (e.g. User.save@100 and Repo.save@200 are distinct keys).
|
|
530
|
+
// Lookup uses a secondary funcName-only index built in lookupReceiverType.
|
|
531
|
+
/** Extract the function name from a scope key ("funcName@startIndex" → "funcName"). */
|
|
532
|
+
const extractFuncNameFromScope = (scope) => scope.slice(0, scope.indexOf('@'));
|
|
533
|
+
/** Extract the trailing function name from a sourceId ("Function:filepath:funcName" → "funcName"). */
|
|
534
|
+
const extractFuncNameFromSourceId = (sourceId) => {
|
|
535
|
+
const lastColon = sourceId.lastIndexOf(':');
|
|
536
|
+
return lastColon >= 0 ? sourceId.slice(lastColon + 1) : '';
|
|
537
|
+
};
|
|
538
|
+
/**
|
|
539
|
+
* Build a composite key for receiver type storage.
|
|
540
|
+
* Uses the full scope string (e.g. "save@100") to distinguish overloaded
|
|
541
|
+
* methods with the same name in different classes.
|
|
542
|
+
*/
|
|
543
|
+
const receiverKey = (scope, varName) => `${scope}\0${varName}`;
|
|
544
|
+
/**
|
|
545
|
+
* Look up a receiver type from a verified receiver map.
|
|
546
|
+
* The map is keyed by `scope\0varName` (full scope with @startIndex).
|
|
547
|
+
* Since the lookup side only has `funcName` (no startIndex), we scan for
|
|
548
|
+
* all entries whose key starts with `funcName@` and has the matching varName.
|
|
549
|
+
* If exactly one unique type is found, return it. If multiple distinct types
|
|
550
|
+
* exist (true overload collision), return undefined (refuse to guess).
|
|
551
|
+
* Falls back to the file-level scope key `\0varName` (empty funcName).
|
|
552
|
+
*/
|
|
553
|
+
const lookupReceiverType = (map, funcName, varName) => {
|
|
554
|
+
// Fast path: file-level scope (empty funcName — used as fallback)
|
|
555
|
+
const fileLevelKey = receiverKey('', varName);
|
|
556
|
+
const prefix = `${funcName}@`;
|
|
557
|
+
const suffix = `\0${varName}`;
|
|
558
|
+
let found;
|
|
559
|
+
let ambiguous = false;
|
|
560
|
+
for (const [key, value] of map) {
|
|
561
|
+
if (key === fileLevelKey)
|
|
562
|
+
continue; // handled separately below
|
|
563
|
+
if (key.startsWith(prefix) && key.endsWith(suffix)) {
|
|
564
|
+
// Verify the key is exactly "funcName@<digits>\0varName" with no extra chars.
|
|
565
|
+
// The part between prefix and suffix should be the startIndex (digits only),
|
|
566
|
+
// but we accept any non-empty segment to be forward-compatible.
|
|
567
|
+
const middle = key.slice(prefix.length, key.length - suffix.length);
|
|
568
|
+
if (middle.length === 0)
|
|
569
|
+
continue; // malformed key — skip
|
|
570
|
+
if (found === undefined) {
|
|
571
|
+
found = value;
|
|
572
|
+
}
|
|
573
|
+
else if (found !== value) {
|
|
574
|
+
ambiguous = true;
|
|
575
|
+
break;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (!ambiguous && found !== undefined)
|
|
580
|
+
return found;
|
|
581
|
+
// Fallback: file-level scope (bindings outside any function)
|
|
582
|
+
return map.get(fileLevelKey);
|
|
583
|
+
};
|
|
254
584
|
/**
|
|
255
585
|
* Fast path: resolve pre-extracted call sites from workers.
|
|
256
586
|
* No AST parsing — workers already extracted calledName + sourceId.
|
|
257
|
-
* This function only does symbol table lookups + graph mutations.
|
|
258
587
|
*/
|
|
259
|
-
export const processCallsFromExtracted = async (graph, extractedCalls,
|
|
260
|
-
//
|
|
588
|
+
export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onProgress, constructorBindings) => {
|
|
589
|
+
// Scope-aware receiver types: keyed by filePath → "funcName\0varName" → typeName.
|
|
590
|
+
// The scope dimension prevents collisions when two functions in the same file
|
|
591
|
+
// have same-named locals pointing to different constructor types.
|
|
592
|
+
const fileReceiverTypes = new Map();
|
|
593
|
+
if (constructorBindings) {
|
|
594
|
+
for (const { filePath, bindings } of constructorBindings) {
|
|
595
|
+
const verified = verifyConstructorBindings(bindings, filePath, ctx, graph);
|
|
596
|
+
if (verified.size > 0) {
|
|
597
|
+
fileReceiverTypes.set(filePath, verified);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
261
601
|
const byFile = new Map();
|
|
262
602
|
for (const call of extractedCalls) {
|
|
263
603
|
let list = byFile.get(call.filePath);
|
|
@@ -269,33 +609,72 @@ export const processCallsFromExtracted = async (graph, extractedCalls, symbolTab
|
|
|
269
609
|
}
|
|
270
610
|
const totalFiles = byFile.size;
|
|
271
611
|
let filesProcessed = 0;
|
|
272
|
-
for (const [
|
|
612
|
+
for (const [filePath, calls] of byFile) {
|
|
273
613
|
filesProcessed++;
|
|
274
614
|
if (filesProcessed % 100 === 0) {
|
|
275
615
|
onProgress?.(filesProcessed, totalFiles);
|
|
276
616
|
await yieldToEventLoop();
|
|
277
617
|
}
|
|
618
|
+
ctx.enableCache(filePath);
|
|
619
|
+
const receiverMap = fileReceiverTypes.get(filePath);
|
|
278
620
|
for (const call of calls) {
|
|
279
|
-
|
|
621
|
+
let effectiveCall = call;
|
|
622
|
+
// Step 1: resolve receiver type from constructor bindings
|
|
623
|
+
if (!call.receiverTypeName && call.receiverName && receiverMap) {
|
|
624
|
+
const callFuncName = extractFuncNameFromSourceId(call.sourceId);
|
|
625
|
+
const resolvedType = lookupReceiverType(receiverMap, callFuncName, call.receiverName);
|
|
626
|
+
if (resolvedType) {
|
|
627
|
+
effectiveCall = { ...call, receiverTypeName: resolvedType };
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Step 1b: class-as-receiver for static method calls (e.g. UserService.find_user())
|
|
631
|
+
if (!effectiveCall.receiverTypeName && effectiveCall.receiverName && effectiveCall.callForm === 'member') {
|
|
632
|
+
const typeResolved = ctx.resolve(effectiveCall.receiverName, effectiveCall.filePath);
|
|
633
|
+
if (typeResolved && typeResolved.candidates.some(d => d.type === 'Class' || d.type === 'Interface' || d.type === 'Struct' || d.type === 'Enum')) {
|
|
634
|
+
effectiveCall = { ...effectiveCall, receiverTypeName: effectiveCall.receiverName };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// Step 2: if the call has a receiver call chain (e.g. svc.getUser().save()),
|
|
638
|
+
// resolve the chain to determine the final receiver type.
|
|
639
|
+
// This runs whenever receiverCallChain is present — even when Step 1 set a
|
|
640
|
+
// receiverTypeName, that type is the BASE receiver (e.g. UserService for svc),
|
|
641
|
+
// and the chain must be walked to produce the FINAL receiver (e.g. User from
|
|
642
|
+
// getUser() : User).
|
|
643
|
+
if (effectiveCall.receiverCallChain?.length) {
|
|
644
|
+
// Step 1 may have resolved the base receiver type (e.g. svc → UserService).
|
|
645
|
+
// Use it as the starting point for chain resolution.
|
|
646
|
+
let baseType = effectiveCall.receiverTypeName;
|
|
647
|
+
// If Step 1 didn't resolve it, try the receiver map directly.
|
|
648
|
+
if (!baseType && effectiveCall.receiverName && receiverMap) {
|
|
649
|
+
const callFuncName = extractFuncNameFromSourceId(effectiveCall.sourceId);
|
|
650
|
+
baseType = lookupReceiverType(receiverMap, callFuncName, effectiveCall.receiverName);
|
|
651
|
+
}
|
|
652
|
+
const chainedType = resolveChainedReceiver(effectiveCall.receiverCallChain, baseType, effectiveCall.filePath, ctx);
|
|
653
|
+
if (chainedType) {
|
|
654
|
+
effectiveCall = { ...effectiveCall, receiverTypeName: chainedType };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
|
|
280
658
|
if (!resolved)
|
|
281
659
|
continue;
|
|
282
|
-
const relId = generateId('CALLS', `${
|
|
660
|
+
const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
|
|
283
661
|
graph.addRelationship({
|
|
284
662
|
id: relId,
|
|
285
|
-
sourceId:
|
|
663
|
+
sourceId: effectiveCall.sourceId,
|
|
286
664
|
targetId: resolved.nodeId,
|
|
287
665
|
type: 'CALLS',
|
|
288
666
|
confidence: resolved.confidence,
|
|
289
667
|
reason: resolved.reason,
|
|
290
668
|
});
|
|
291
669
|
}
|
|
670
|
+
ctx.clearCache();
|
|
292
671
|
}
|
|
293
672
|
onProgress?.(totalFiles, totalFiles);
|
|
294
673
|
};
|
|
295
674
|
/**
|
|
296
675
|
* Resolve pre-extracted Laravel routes to CALLS edges from route files to controller methods.
|
|
297
676
|
*/
|
|
298
|
-
export const processRoutesFromExtracted = async (graph, extractedRoutes,
|
|
677
|
+
export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, onProgress) => {
|
|
299
678
|
for (let i = 0; i < extractedRoutes.length; i++) {
|
|
300
679
|
const route = extractedRoutes[i];
|
|
301
680
|
if (i % 50 === 0) {
|
|
@@ -304,21 +683,17 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, symbolT
|
|
|
304
683
|
}
|
|
305
684
|
if (!route.controllerName || !route.methodName)
|
|
306
685
|
continue;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const resolution = resolveSymbolInternal(route.controllerName, route.filePath, symbolTable, importMap, packageMap);
|
|
310
|
-
if (!resolution)
|
|
686
|
+
const controllerResolved = ctx.resolve(route.controllerName, route.filePath);
|
|
687
|
+
if (!controllerResolved || controllerResolved.candidates.length === 0)
|
|
311
688
|
continue;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const methodId = symbolTable.lookupExact(controllerDef.filePath, route.methodName);
|
|
689
|
+
if (controllerResolved.tier === 'global' && controllerResolved.candidates.length > 1)
|
|
690
|
+
continue;
|
|
691
|
+
const controllerDef = controllerResolved.candidates[0];
|
|
692
|
+
const confidence = TIER_CONFIDENCE[controllerResolved.tier];
|
|
693
|
+
const methodResolved = ctx.resolve(route.methodName, controllerDef.filePath);
|
|
694
|
+
const methodId = methodResolved?.tier === 'same-file' ? methodResolved.candidates[0]?.nodeId : undefined;
|
|
319
695
|
const sourceId = generateId('File', route.filePath);
|
|
320
696
|
if (!methodId) {
|
|
321
|
-
// Construct method ID manually
|
|
322
697
|
const guessedId = generateId('Method', `${controllerDef.filePath}:${route.methodName}`);
|
|
323
698
|
const relId = generateId('CALLS', `${sourceId}:route->${guessedId}`);
|
|
324
699
|
graph.addRelationship({
|
|
@@ -343,15 +718,3 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, symbolT
|
|
|
343
718
|
}
|
|
344
719
|
onProgress?.(extractedRoutes.length, extractedRoutes.length);
|
|
345
720
|
};
|
|
346
|
-
/**
|
|
347
|
-
* Follow re-export chains through NamedImportMap for call candidate collection.
|
|
348
|
-
* Delegates chain-walking to the shared walkBindingChain utility, then
|
|
349
|
-
* applies call-processor semantics: any number of matches accepted.
|
|
350
|
-
*/
|
|
351
|
-
const resolveNamedBindingChainForCandidates = (calledName, currentFile, symbolTable, namedImportMap, allDefs) => {
|
|
352
|
-
const defs = walkBindingChain(calledName, currentFile, symbolTable, namedImportMap, allDefs);
|
|
353
|
-
if (defs && defs.length > 0) {
|
|
354
|
-
return { candidates: defs, tier: 'import-scoped' };
|
|
355
|
-
}
|
|
356
|
-
return null;
|
|
357
|
-
};
|