@softerist/heuristic-mcp 3.2.2 → 3.2.4
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 +387 -376
- package/config.jsonc +800 -800
- package/features/ann-config.js +102 -110
- package/features/clear-cache.js +81 -84
- package/features/find-similar-code.js +265 -286
- package/features/hybrid-search.js +487 -536
- package/features/index-codebase.js +3139 -3270
- package/features/lifecycle.js +1041 -1063
- package/features/package-version.js +277 -291
- package/features/register.js +351 -370
- package/features/resources.js +115 -130
- package/features/set-workspace.js +214 -240
- package/index.js +742 -762
- package/lib/cache-ops.js +22 -22
- package/lib/cache-utils.js +465 -519
- package/lib/cache.js +1699 -1767
- package/lib/call-graph.js +396 -396
- package/lib/cli.js +232 -226
- package/lib/config.js +1483 -1495
- package/lib/constants.js +511 -492
- package/lib/embed-query-process.js +206 -212
- package/lib/embedding-process.js +434 -451
- package/lib/embedding-worker.js +862 -934
- package/lib/ignore-patterns.js +276 -316
- package/lib/json-worker.js +14 -14
- package/lib/json-writer.js +302 -310
- package/lib/logging.js +116 -127
- package/lib/memory-logger.js +13 -13
- package/lib/onnx-backend.js +188 -193
- package/lib/path-utils.js +18 -23
- package/lib/project-detector.js +82 -84
- package/lib/server-lifecycle.js +133 -145
- package/lib/settings-editor.js +738 -739
- package/lib/slice-normalize.js +25 -31
- package/lib/tokenizer.js +168 -203
- package/lib/utils.js +364 -409
- package/lib/vector-store-binary.js +811 -591
- package/lib/vector-store-sqlite.js +377 -414
- package/lib/workspace-env.js +32 -34
- package/mcp_config.json +9 -9
- package/package.json +86 -86
- package/scripts/clear-cache.js +20 -20
- package/scripts/download-model.js +43 -43
- package/scripts/mcp-launcher.js +49 -49
- package/scripts/postinstall.js +12 -12
- package/search-configs.js +36 -36
package/lib/call-graph.js
CHANGED
|
@@ -1,396 +1,396 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Call Graph Extractor
|
|
3
|
-
*
|
|
4
|
-
* Lightweight regex-based extraction of function definitions and calls.
|
|
5
|
-
* Works across multiple languages without external dependencies.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import path from 'path';
|
|
9
|
-
|
|
10
|
-
// Language-specific patterns for function/method definitions
|
|
11
|
-
const DEFINITION_PATTERNS = {
|
|
12
|
-
javascript: [
|
|
13
|
-
// function declarations: function name() or async function name()
|
|
14
|
-
/(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
|
|
15
|
-
// arrow functions: const name = () => or const name = async () =>
|
|
16
|
-
/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g,
|
|
17
|
-
// class declarations
|
|
18
|
-
/class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g,
|
|
19
|
-
// method definitions: name() { or async name() {
|
|
20
|
-
/^\s*(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/gm,
|
|
21
|
-
// object method shorthand: name() { inside object
|
|
22
|
-
/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/g,
|
|
23
|
-
],
|
|
24
|
-
python: [
|
|
25
|
-
// def name():
|
|
26
|
-
/def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g,
|
|
27
|
-
// class Name:
|
|
28
|
-
/class\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[:(]/g,
|
|
29
|
-
],
|
|
30
|
-
go: [
|
|
31
|
-
// func name() or func (r Receiver) name()
|
|
32
|
-
/func\s+(?:\([^)]*\)\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g,
|
|
33
|
-
],
|
|
34
|
-
rust: [
|
|
35
|
-
// fn name()
|
|
36
|
-
/fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[<(]/g,
|
|
37
|
-
// impl Name
|
|
38
|
-
/impl(?:\s*<[^>]*>)?\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
39
|
-
],
|
|
40
|
-
java: [
|
|
41
|
-
// public void name() or private static String name()
|
|
42
|
-
/(?:public|private|protected)?\s*(?:static)?\s*(?:\w+)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g,
|
|
43
|
-
// class Name
|
|
44
|
-
/class\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
45
|
-
],
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// Pattern for function calls (language-agnostic, catches most cases)
|
|
49
|
-
const CALL_PATTERN = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
50
|
-
|
|
51
|
-
// Common built-ins to exclude from call detection (all lowercase for case-insensitive matching)
|
|
52
|
-
const BUILTIN_EXCLUSIONS = new Set([
|
|
53
|
-
// JavaScript
|
|
54
|
-
'if',
|
|
55
|
-
'for',
|
|
56
|
-
'while',
|
|
57
|
-
'switch',
|
|
58
|
-
'catch',
|
|
59
|
-
'function',
|
|
60
|
-
'async',
|
|
61
|
-
'await',
|
|
62
|
-
'return',
|
|
63
|
-
'throw',
|
|
64
|
-
'new',
|
|
65
|
-
'typeof',
|
|
66
|
-
'instanceof',
|
|
67
|
-
'delete',
|
|
68
|
-
'void',
|
|
69
|
-
'console',
|
|
70
|
-
'require',
|
|
71
|
-
'import',
|
|
72
|
-
'export',
|
|
73
|
-
'super',
|
|
74
|
-
'this',
|
|
75
|
-
// Common functions that aren't meaningful for call graphs
|
|
76
|
-
'parseint',
|
|
77
|
-
'parsefloat',
|
|
78
|
-
'string',
|
|
79
|
-
'number',
|
|
80
|
-
'boolean',
|
|
81
|
-
'array',
|
|
82
|
-
'object',
|
|
83
|
-
'map',
|
|
84
|
-
'set',
|
|
85
|
-
'promise',
|
|
86
|
-
'error',
|
|
87
|
-
'json',
|
|
88
|
-
'math',
|
|
89
|
-
'date',
|
|
90
|
-
'regexp',
|
|
91
|
-
// Python
|
|
92
|
-
'def',
|
|
93
|
-
'class',
|
|
94
|
-
'print',
|
|
95
|
-
'len',
|
|
96
|
-
'range',
|
|
97
|
-
'str',
|
|
98
|
-
'int',
|
|
99
|
-
'float',
|
|
100
|
-
'list',
|
|
101
|
-
'dict',
|
|
102
|
-
'tuple',
|
|
103
|
-
'bool',
|
|
104
|
-
'type',
|
|
105
|
-
'isinstance',
|
|
106
|
-
'hasattr',
|
|
107
|
-
'getattr',
|
|
108
|
-
'setattr',
|
|
109
|
-
// Go
|
|
110
|
-
'func',
|
|
111
|
-
'make',
|
|
112
|
-
'append',
|
|
113
|
-
'cap',
|
|
114
|
-
'panic',
|
|
115
|
-
'recover',
|
|
116
|
-
// Control flow that looks like function calls
|
|
117
|
-
'else',
|
|
118
|
-
'try',
|
|
119
|
-
'finally',
|
|
120
|
-
'with',
|
|
121
|
-
'assert',
|
|
122
|
-
'raise',
|
|
123
|
-
'yield',
|
|
124
|
-
// Test frameworks
|
|
125
|
-
'describe',
|
|
126
|
-
'it',
|
|
127
|
-
'test',
|
|
128
|
-
'expect',
|
|
129
|
-
'beforeeach',
|
|
130
|
-
'aftereach',
|
|
131
|
-
'beforeall',
|
|
132
|
-
'afterall',
|
|
133
|
-
// Common prototypes / methods (too noisy)
|
|
134
|
-
'match',
|
|
135
|
-
'exec',
|
|
136
|
-
'replace',
|
|
137
|
-
'split',
|
|
138
|
-
'join',
|
|
139
|
-
'slice',
|
|
140
|
-
'splice',
|
|
141
|
-
'push',
|
|
142
|
-
'pop',
|
|
143
|
-
'shift',
|
|
144
|
-
'unshift',
|
|
145
|
-
'includes',
|
|
146
|
-
'indexof',
|
|
147
|
-
'foreach',
|
|
148
|
-
'filter',
|
|
149
|
-
'reduce',
|
|
150
|
-
'find',
|
|
151
|
-
'some',
|
|
152
|
-
'every',
|
|
153
|
-
'sort',
|
|
154
|
-
'keys',
|
|
155
|
-
'values',
|
|
156
|
-
'entries',
|
|
157
|
-
'from',
|
|
158
|
-
'then',
|
|
159
|
-
'catch',
|
|
160
|
-
'finally',
|
|
161
|
-
'all',
|
|
162
|
-
'race',
|
|
163
|
-
'resolve',
|
|
164
|
-
'reject',
|
|
165
|
-
]);
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Detect language from file extension
|
|
169
|
-
*/
|
|
170
|
-
function detectLanguage(file) {
|
|
171
|
-
const ext = path.extname(file).toLowerCase();
|
|
172
|
-
const langMap = {
|
|
173
|
-
'.js': 'javascript',
|
|
174
|
-
'.jsx': 'javascript',
|
|
175
|
-
'.ts': 'javascript',
|
|
176
|
-
'.tsx': 'javascript',
|
|
177
|
-
'.mjs': 'javascript',
|
|
178
|
-
'.cjs': 'javascript',
|
|
179
|
-
'.py': 'python',
|
|
180
|
-
'.pyw': 'python',
|
|
181
|
-
'.go': 'go',
|
|
182
|
-
'.rs': 'rust',
|
|
183
|
-
'.java': 'java',
|
|
184
|
-
'.kt': 'java',
|
|
185
|
-
'.scala': 'java',
|
|
186
|
-
};
|
|
187
|
-
if (langMap[ext]) {
|
|
188
|
-
return langMap[ext];
|
|
189
|
-
} else {
|
|
190
|
-
return 'javascript'; // Default to JS patterns
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Extract function/class definitions from content
|
|
196
|
-
* Exported for testing; treat as internal helper.
|
|
197
|
-
*/
|
|
198
|
-
export function extractDefinitions(content, file) {
|
|
199
|
-
const language = detectLanguage(file);
|
|
200
|
-
const patterns = DEFINITION_PATTERNS[language];
|
|
201
|
-
const definitions = new Set();
|
|
202
|
-
|
|
203
|
-
for (const pattern of patterns) {
|
|
204
|
-
// Reset regex state
|
|
205
|
-
pattern.lastIndex = 0;
|
|
206
|
-
let match;
|
|
207
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
208
|
-
const name = match[1];
|
|
209
|
-
if (name && name.length > 1 && !BUILTIN_EXCLUSIONS.has(name.toLowerCase())) {
|
|
210
|
-
definitions.add(name);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return Array.from(definitions);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Extract function calls from content
|
|
220
|
-
* Exported for testing; treat as internal helper.
|
|
221
|
-
*/
|
|
222
|
-
export function extractCalls(content, file) {
|
|
223
|
-
const calls = new Set();
|
|
224
|
-
|
|
225
|
-
// Remove string literals and comments to avoid false positives
|
|
226
|
-
const cleanContent = removeStringsAndComments(content, file);
|
|
227
|
-
|
|
228
|
-
CALL_PATTERN.lastIndex = 0;
|
|
229
|
-
let match;
|
|
230
|
-
while ((match = CALL_PATTERN.exec(cleanContent)) !== null) {
|
|
231
|
-
const name = match[1];
|
|
232
|
-
if (name && name.length > 1 && !BUILTIN_EXCLUSIONS.has(name.toLowerCase())) {
|
|
233
|
-
calls.add(name);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return Array.from(calls);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Remove string literals and comments to improve extraction accuracy
|
|
242
|
-
*/
|
|
243
|
-
function removeStringsAndComments(content, file) {
|
|
244
|
-
const ext = path.extname(file).toLowerCase();
|
|
245
|
-
|
|
246
|
-
// Remove single-line comments
|
|
247
|
-
let cleaned = content.replace(/\/\/.*$/gm, '');
|
|
248
|
-
|
|
249
|
-
// Remove multi-line comments
|
|
250
|
-
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
251
|
-
|
|
252
|
-
// Remove Python comments
|
|
253
|
-
if (ext === '.py' || ext === '.pyw') {
|
|
254
|
-
cleaned = cleaned.replace(/#.*$/gm, '');
|
|
255
|
-
// Remove triple-quoted strings (docstrings)
|
|
256
|
-
cleaned = cleaned.replace(/"""[\s\S]*?"""/g, '');
|
|
257
|
-
cleaned = cleaned.replace(/'''[\s\S]*?'''/g, '');
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Remove string literals (simplified - handles most cases)
|
|
261
|
-
cleaned = cleaned.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
262
|
-
cleaned = cleaned.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
263
|
-
cleaned = cleaned.replace(/`(?:[^`\\]|\\.)*`/g, '``');
|
|
264
|
-
|
|
265
|
-
return cleaned;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Extract both definitions and calls from a file
|
|
270
|
-
*/
|
|
271
|
-
export function extractCallData(content, file) {
|
|
272
|
-
const definitions = extractDefinitions(content, file);
|
|
273
|
-
const calls = extractCalls(content, file);
|
|
274
|
-
|
|
275
|
-
// Remove self-references (calls to functions defined in same file)
|
|
276
|
-
const definitionSet = new Set(definitions);
|
|
277
|
-
const externalCalls = calls.filter((c) => !definitionSet.has(c));
|
|
278
|
-
|
|
279
|
-
return {
|
|
280
|
-
definitions,
|
|
281
|
-
calls: externalCalls,
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Build a call graph from file data
|
|
287
|
-
*/
|
|
288
|
-
export function buildCallGraph(fileCallData) {
|
|
289
|
-
const defines = new Map(); // symbol -> files that define it
|
|
290
|
-
const calledBy = new Map(); // symbol -> files that call it
|
|
291
|
-
const fileCalls = new Map(); // file -> symbols it calls
|
|
292
|
-
|
|
293
|
-
for (const [file, data] of fileCallData.entries()) {
|
|
294
|
-
// Record definitions
|
|
295
|
-
for (const def of data.definitions) {
|
|
296
|
-
if (!defines.has(def)) {
|
|
297
|
-
defines.set(def, []);
|
|
298
|
-
}
|
|
299
|
-
defines.get(def).push(file);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Record calls
|
|
303
|
-
fileCalls.set(file, data.calls);
|
|
304
|
-
for (const call of data.calls) {
|
|
305
|
-
if (!calledBy.has(call)) {
|
|
306
|
-
calledBy.set(call, []);
|
|
307
|
-
}
|
|
308
|
-
calledBy.get(call).push(file);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return { defines, calledBy, fileCalls };
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Get files related to a set of symbols (callers + callees)
|
|
317
|
-
*/
|
|
318
|
-
export function getRelatedFiles(callGraph, symbols, maxHops = 1) {
|
|
319
|
-
const related = new Map(); // file -> proximity score (1 = direct, 0.5 = indirect)
|
|
320
|
-
const visited = new Set();
|
|
321
|
-
|
|
322
|
-
function explore(currentSymbols, hop) {
|
|
323
|
-
if (hop > maxHops) return;
|
|
324
|
-
const score = 1 / (hop + 1); // Decay with distance
|
|
325
|
-
|
|
326
|
-
for (const symbol of currentSymbols) {
|
|
327
|
-
// Files that define this symbol
|
|
328
|
-
const definers = callGraph.defines.get(symbol) || [];
|
|
329
|
-
for (const file of definers) {
|
|
330
|
-
if (!visited.has(file)) {
|
|
331
|
-
related.set(file, Math.max(related.get(file) || 0, score));
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Files that call this symbol
|
|
336
|
-
const callers = callGraph.calledBy.get(symbol) || [];
|
|
337
|
-
for (const file of callers) {
|
|
338
|
-
if (!visited.has(file)) {
|
|
339
|
-
related.set(file, Math.max(related.get(file) || 0, score));
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// For next hop, find what these files call/define
|
|
344
|
-
if (hop < maxHops) {
|
|
345
|
-
const nextSymbols = new Set();
|
|
346
|
-
for (const file of [...definers, ...callers]) {
|
|
347
|
-
visited.add(file);
|
|
348
|
-
const calls = callGraph.fileCalls.get(file) || [];
|
|
349
|
-
for (const c of calls) nextSymbols.add(c);
|
|
350
|
-
}
|
|
351
|
-
explore(nextSymbols, hop + 1);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
explore(symbols, 0);
|
|
357
|
-
return related;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Patterns for extracting symbols from content
|
|
361
|
-
const SYMBOL_PATTERNS = [
|
|
362
|
-
// function name()
|
|
363
|
-
/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g,
|
|
364
|
-
// class Name
|
|
365
|
-
/class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g,
|
|
366
|
-
// const/let/var name = ...
|
|
367
|
-
/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g,
|
|
368
|
-
// Python def/class
|
|
369
|
-
/def\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
370
|
-
/class\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
371
|
-
// Go func
|
|
372
|
-
/func\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
373
|
-
// Rust fn
|
|
374
|
-
/fn\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
375
|
-
// Java/C# methods (simplified)
|
|
376
|
-
/(?:public|private|protected|static)\s+\w+\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g,
|
|
377
|
-
];
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Extract symbols (function/class names) from search results
|
|
381
|
-
*/
|
|
382
|
-
export function extractSymbolsFromContent(content) {
|
|
383
|
-
const symbols = new Set();
|
|
384
|
-
|
|
385
|
-
for (const pattern of SYMBOL_PATTERNS) {
|
|
386
|
-
pattern.lastIndex = 0;
|
|
387
|
-
let match;
|
|
388
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
389
|
-
if (match[1] && match[1].length > 2) {
|
|
390
|
-
symbols.add(match[1]);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return Array.from(symbols);
|
|
396
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Call Graph Extractor
|
|
3
|
+
*
|
|
4
|
+
* Lightweight regex-based extraction of function definitions and calls.
|
|
5
|
+
* Works across multiple languages without external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
// Language-specific patterns for function/method definitions
|
|
11
|
+
const DEFINITION_PATTERNS = {
|
|
12
|
+
javascript: [
|
|
13
|
+
// function declarations: function name() or async function name()
|
|
14
|
+
/(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
|
|
15
|
+
// arrow functions: const name = () => or const name = async () =>
|
|
16
|
+
/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g,
|
|
17
|
+
// class declarations
|
|
18
|
+
/class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g,
|
|
19
|
+
// method definitions: name() { or async name() {
|
|
20
|
+
/^\s*(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/gm,
|
|
21
|
+
// object method shorthand: name() { inside object
|
|
22
|
+
/([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/g,
|
|
23
|
+
],
|
|
24
|
+
python: [
|
|
25
|
+
// def name():
|
|
26
|
+
/def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g,
|
|
27
|
+
// class Name:
|
|
28
|
+
/class\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[:(]/g,
|
|
29
|
+
],
|
|
30
|
+
go: [
|
|
31
|
+
// func name() or func (r Receiver) name()
|
|
32
|
+
/func\s+(?:\([^)]*\)\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g,
|
|
33
|
+
],
|
|
34
|
+
rust: [
|
|
35
|
+
// fn name()
|
|
36
|
+
/fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[<(]/g,
|
|
37
|
+
// impl Name
|
|
38
|
+
/impl(?:\s*<[^>]*>)?\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
39
|
+
],
|
|
40
|
+
java: [
|
|
41
|
+
// public void name() or private static String name()
|
|
42
|
+
/(?:public|private|protected)?\s*(?:static)?\s*(?:\w+)\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g,
|
|
43
|
+
// class Name
|
|
44
|
+
/class\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Pattern for function calls (language-agnostic, catches most cases)
|
|
49
|
+
const CALL_PATTERN = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
50
|
+
|
|
51
|
+
// Common built-ins to exclude from call detection (all lowercase for case-insensitive matching)
|
|
52
|
+
const BUILTIN_EXCLUSIONS = new Set([
|
|
53
|
+
// JavaScript
|
|
54
|
+
'if',
|
|
55
|
+
'for',
|
|
56
|
+
'while',
|
|
57
|
+
'switch',
|
|
58
|
+
'catch',
|
|
59
|
+
'function',
|
|
60
|
+
'async',
|
|
61
|
+
'await',
|
|
62
|
+
'return',
|
|
63
|
+
'throw',
|
|
64
|
+
'new',
|
|
65
|
+
'typeof',
|
|
66
|
+
'instanceof',
|
|
67
|
+
'delete',
|
|
68
|
+
'void',
|
|
69
|
+
'console',
|
|
70
|
+
'require',
|
|
71
|
+
'import',
|
|
72
|
+
'export',
|
|
73
|
+
'super',
|
|
74
|
+
'this',
|
|
75
|
+
// Common functions that aren't meaningful for call graphs
|
|
76
|
+
'parseint',
|
|
77
|
+
'parsefloat',
|
|
78
|
+
'string',
|
|
79
|
+
'number',
|
|
80
|
+
'boolean',
|
|
81
|
+
'array',
|
|
82
|
+
'object',
|
|
83
|
+
'map',
|
|
84
|
+
'set',
|
|
85
|
+
'promise',
|
|
86
|
+
'error',
|
|
87
|
+
'json',
|
|
88
|
+
'math',
|
|
89
|
+
'date',
|
|
90
|
+
'regexp',
|
|
91
|
+
// Python
|
|
92
|
+
'def',
|
|
93
|
+
'class',
|
|
94
|
+
'print',
|
|
95
|
+
'len',
|
|
96
|
+
'range',
|
|
97
|
+
'str',
|
|
98
|
+
'int',
|
|
99
|
+
'float',
|
|
100
|
+
'list',
|
|
101
|
+
'dict',
|
|
102
|
+
'tuple',
|
|
103
|
+
'bool',
|
|
104
|
+
'type',
|
|
105
|
+
'isinstance',
|
|
106
|
+
'hasattr',
|
|
107
|
+
'getattr',
|
|
108
|
+
'setattr',
|
|
109
|
+
// Go
|
|
110
|
+
'func',
|
|
111
|
+
'make',
|
|
112
|
+
'append',
|
|
113
|
+
'cap',
|
|
114
|
+
'panic',
|
|
115
|
+
'recover',
|
|
116
|
+
// Control flow that looks like function calls
|
|
117
|
+
'else',
|
|
118
|
+
'try',
|
|
119
|
+
'finally',
|
|
120
|
+
'with',
|
|
121
|
+
'assert',
|
|
122
|
+
'raise',
|
|
123
|
+
'yield',
|
|
124
|
+
// Test frameworks
|
|
125
|
+
'describe',
|
|
126
|
+
'it',
|
|
127
|
+
'test',
|
|
128
|
+
'expect',
|
|
129
|
+
'beforeeach',
|
|
130
|
+
'aftereach',
|
|
131
|
+
'beforeall',
|
|
132
|
+
'afterall',
|
|
133
|
+
// Common prototypes / methods (too noisy)
|
|
134
|
+
'match',
|
|
135
|
+
'exec',
|
|
136
|
+
'replace',
|
|
137
|
+
'split',
|
|
138
|
+
'join',
|
|
139
|
+
'slice',
|
|
140
|
+
'splice',
|
|
141
|
+
'push',
|
|
142
|
+
'pop',
|
|
143
|
+
'shift',
|
|
144
|
+
'unshift',
|
|
145
|
+
'includes',
|
|
146
|
+
'indexof',
|
|
147
|
+
'foreach',
|
|
148
|
+
'filter',
|
|
149
|
+
'reduce',
|
|
150
|
+
'find',
|
|
151
|
+
'some',
|
|
152
|
+
'every',
|
|
153
|
+
'sort',
|
|
154
|
+
'keys',
|
|
155
|
+
'values',
|
|
156
|
+
'entries',
|
|
157
|
+
'from',
|
|
158
|
+
'then',
|
|
159
|
+
'catch',
|
|
160
|
+
'finally',
|
|
161
|
+
'all',
|
|
162
|
+
'race',
|
|
163
|
+
'resolve',
|
|
164
|
+
'reject',
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Detect language from file extension
|
|
169
|
+
*/
|
|
170
|
+
function detectLanguage(file) {
|
|
171
|
+
const ext = path.extname(file).toLowerCase();
|
|
172
|
+
const langMap = {
|
|
173
|
+
'.js': 'javascript',
|
|
174
|
+
'.jsx': 'javascript',
|
|
175
|
+
'.ts': 'javascript',
|
|
176
|
+
'.tsx': 'javascript',
|
|
177
|
+
'.mjs': 'javascript',
|
|
178
|
+
'.cjs': 'javascript',
|
|
179
|
+
'.py': 'python',
|
|
180
|
+
'.pyw': 'python',
|
|
181
|
+
'.go': 'go',
|
|
182
|
+
'.rs': 'rust',
|
|
183
|
+
'.java': 'java',
|
|
184
|
+
'.kt': 'java',
|
|
185
|
+
'.scala': 'java',
|
|
186
|
+
};
|
|
187
|
+
if (langMap[ext]) {
|
|
188
|
+
return langMap[ext];
|
|
189
|
+
} else {
|
|
190
|
+
return 'javascript'; // Default to JS patterns
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Extract function/class definitions from content
|
|
196
|
+
* Exported for testing; treat as internal helper.
|
|
197
|
+
*/
|
|
198
|
+
export function extractDefinitions(content, file) {
|
|
199
|
+
const language = detectLanguage(file);
|
|
200
|
+
const patterns = DEFINITION_PATTERNS[language];
|
|
201
|
+
const definitions = new Set();
|
|
202
|
+
|
|
203
|
+
for (const pattern of patterns) {
|
|
204
|
+
// Reset regex state
|
|
205
|
+
pattern.lastIndex = 0;
|
|
206
|
+
let match;
|
|
207
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
208
|
+
const name = match[1];
|
|
209
|
+
if (name && name.length > 1 && !BUILTIN_EXCLUSIONS.has(name.toLowerCase())) {
|
|
210
|
+
definitions.add(name);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return Array.from(definitions);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Extract function calls from content
|
|
220
|
+
* Exported for testing; treat as internal helper.
|
|
221
|
+
*/
|
|
222
|
+
export function extractCalls(content, file) {
|
|
223
|
+
const calls = new Set();
|
|
224
|
+
|
|
225
|
+
// Remove string literals and comments to avoid false positives
|
|
226
|
+
const cleanContent = removeStringsAndComments(content, file);
|
|
227
|
+
|
|
228
|
+
CALL_PATTERN.lastIndex = 0;
|
|
229
|
+
let match;
|
|
230
|
+
while ((match = CALL_PATTERN.exec(cleanContent)) !== null) {
|
|
231
|
+
const name = match[1];
|
|
232
|
+
if (name && name.length > 1 && !BUILTIN_EXCLUSIONS.has(name.toLowerCase())) {
|
|
233
|
+
calls.add(name);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return Array.from(calls);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Remove string literals and comments to improve extraction accuracy
|
|
242
|
+
*/
|
|
243
|
+
function removeStringsAndComments(content, file) {
|
|
244
|
+
const ext = path.extname(file).toLowerCase();
|
|
245
|
+
|
|
246
|
+
// Remove single-line comments
|
|
247
|
+
let cleaned = content.replace(/\/\/.*$/gm, '');
|
|
248
|
+
|
|
249
|
+
// Remove multi-line comments
|
|
250
|
+
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
251
|
+
|
|
252
|
+
// Remove Python comments
|
|
253
|
+
if (ext === '.py' || ext === '.pyw') {
|
|
254
|
+
cleaned = cleaned.replace(/#.*$/gm, '');
|
|
255
|
+
// Remove triple-quoted strings (docstrings)
|
|
256
|
+
cleaned = cleaned.replace(/"""[\s\S]*?"""/g, '');
|
|
257
|
+
cleaned = cleaned.replace(/'''[\s\S]*?'''/g, '');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Remove string literals (simplified - handles most cases)
|
|
261
|
+
cleaned = cleaned.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
262
|
+
cleaned = cleaned.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
263
|
+
cleaned = cleaned.replace(/`(?:[^`\\]|\\.)*`/g, '``');
|
|
264
|
+
|
|
265
|
+
return cleaned;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extract both definitions and calls from a file
|
|
270
|
+
*/
|
|
271
|
+
export function extractCallData(content, file) {
|
|
272
|
+
const definitions = extractDefinitions(content, file);
|
|
273
|
+
const calls = extractCalls(content, file);
|
|
274
|
+
|
|
275
|
+
// Remove self-references (calls to functions defined in same file)
|
|
276
|
+
const definitionSet = new Set(definitions);
|
|
277
|
+
const externalCalls = calls.filter((c) => !definitionSet.has(c));
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
definitions,
|
|
281
|
+
calls: externalCalls,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Build a call graph from file data
|
|
287
|
+
*/
|
|
288
|
+
export function buildCallGraph(fileCallData) {
|
|
289
|
+
const defines = new Map(); // symbol -> files that define it
|
|
290
|
+
const calledBy = new Map(); // symbol -> files that call it
|
|
291
|
+
const fileCalls = new Map(); // file -> symbols it calls
|
|
292
|
+
|
|
293
|
+
for (const [file, data] of fileCallData.entries()) {
|
|
294
|
+
// Record definitions
|
|
295
|
+
for (const def of data.definitions) {
|
|
296
|
+
if (!defines.has(def)) {
|
|
297
|
+
defines.set(def, []);
|
|
298
|
+
}
|
|
299
|
+
defines.get(def).push(file);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Record calls
|
|
303
|
+
fileCalls.set(file, data.calls);
|
|
304
|
+
for (const call of data.calls) {
|
|
305
|
+
if (!calledBy.has(call)) {
|
|
306
|
+
calledBy.set(call, []);
|
|
307
|
+
}
|
|
308
|
+
calledBy.get(call).push(file);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return { defines, calledBy, fileCalls };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Get files related to a set of symbols (callers + callees)
|
|
317
|
+
*/
|
|
318
|
+
export function getRelatedFiles(callGraph, symbols, maxHops = 1) {
|
|
319
|
+
const related = new Map(); // file -> proximity score (1 = direct, 0.5 = indirect)
|
|
320
|
+
const visited = new Set();
|
|
321
|
+
|
|
322
|
+
function explore(currentSymbols, hop) {
|
|
323
|
+
if (hop > maxHops) return;
|
|
324
|
+
const score = 1 / (hop + 1); // Decay with distance
|
|
325
|
+
|
|
326
|
+
for (const symbol of currentSymbols) {
|
|
327
|
+
// Files that define this symbol
|
|
328
|
+
const definers = callGraph.defines.get(symbol) || [];
|
|
329
|
+
for (const file of definers) {
|
|
330
|
+
if (!visited.has(file)) {
|
|
331
|
+
related.set(file, Math.max(related.get(file) || 0, score));
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Files that call this symbol
|
|
336
|
+
const callers = callGraph.calledBy.get(symbol) || [];
|
|
337
|
+
for (const file of callers) {
|
|
338
|
+
if (!visited.has(file)) {
|
|
339
|
+
related.set(file, Math.max(related.get(file) || 0, score));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// For next hop, find what these files call/define
|
|
344
|
+
if (hop < maxHops) {
|
|
345
|
+
const nextSymbols = new Set();
|
|
346
|
+
for (const file of [...definers, ...callers]) {
|
|
347
|
+
visited.add(file);
|
|
348
|
+
const calls = callGraph.fileCalls.get(file) || [];
|
|
349
|
+
for (const c of calls) nextSymbols.add(c);
|
|
350
|
+
}
|
|
351
|
+
explore(nextSymbols, hop + 1);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
explore(symbols, 0);
|
|
357
|
+
return related;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Patterns for extracting symbols from content
|
|
361
|
+
const SYMBOL_PATTERNS = [
|
|
362
|
+
// function name()
|
|
363
|
+
/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g,
|
|
364
|
+
// class Name
|
|
365
|
+
/class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g,
|
|
366
|
+
// const/let/var name = ...
|
|
367
|
+
/(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g,
|
|
368
|
+
// Python def/class
|
|
369
|
+
/def\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
370
|
+
/class\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
371
|
+
// Go func
|
|
372
|
+
/func\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
373
|
+
// Rust fn
|
|
374
|
+
/fn\s+([a-zA-Z_][a-zA-Z0-9_]*)/g,
|
|
375
|
+
// Java/C# methods (simplified)
|
|
376
|
+
/(?:public|private|protected|static)\s+\w+\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g,
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Extract symbols (function/class names) from search results
|
|
381
|
+
*/
|
|
382
|
+
export function extractSymbolsFromContent(content) {
|
|
383
|
+
const symbols = new Set();
|
|
384
|
+
|
|
385
|
+
for (const pattern of SYMBOL_PATTERNS) {
|
|
386
|
+
pattern.lastIndex = 0;
|
|
387
|
+
let match;
|
|
388
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
389
|
+
if (match[1] && match[1].length > 2) {
|
|
390
|
+
symbols.add(match[1]);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return Array.from(symbols);
|
|
396
|
+
}
|