spec-gen-cli 1.2.2 → 1.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 +272 -25
- package/dist/api/generate.d.ts.map +1 -1
- package/dist/api/generate.js +11 -7
- package/dist/api/generate.js.map +1 -1
- package/dist/api/run.d.ts.map +1 -1
- package/dist/api/run.js +5 -3
- package/dist/api/run.js.map +1 -1
- package/dist/api/types.d.ts +4 -4
- package/dist/api/types.d.ts.map +1 -1
- package/dist/cli/commands/analyze.d.ts.map +1 -1
- package/dist/cli/commands/analyze.js +101 -41
- package/dist/cli/commands/analyze.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +28 -23
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts +353 -10
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +241 -48
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/view.d.ts.map +1 -1
- package/dist/cli/commands/view.js +33 -4
- package/dist/cli/commands/view.js.map +1 -1
- package/dist/constants.d.ts +11 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +11 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/analyzer/artifact-generator.d.ts.map +1 -1
- package/dist/core/analyzer/artifact-generator.js +11 -3
- package/dist/core/analyzer/artifact-generator.js.map +1 -1
- package/dist/core/analyzer/ast-chunker.d.ts +24 -0
- package/dist/core/analyzer/ast-chunker.d.ts.map +1 -0
- package/dist/core/analyzer/ast-chunker.js +198 -0
- package/dist/core/analyzer/ast-chunker.js.map +1 -0
- package/dist/core/analyzer/call-graph.d.ts +52 -5
- package/dist/core/analyzer/call-graph.d.ts.map +1 -1
- package/dist/core/analyzer/call-graph.js +769 -48
- package/dist/core/analyzer/call-graph.js.map +1 -1
- package/dist/core/analyzer/code-shaper.d.ts.map +1 -1
- package/dist/core/analyzer/code-shaper.js +5 -0
- package/dist/core/analyzer/code-shaper.js.map +1 -1
- package/dist/core/analyzer/codebase-digest.d.ts +40 -0
- package/dist/core/analyzer/codebase-digest.d.ts.map +1 -0
- package/dist/core/analyzer/codebase-digest.js +194 -0
- package/dist/core/analyzer/codebase-digest.js.map +1 -0
- package/dist/core/analyzer/cpp-header-resolver.d.ts +30 -0
- package/dist/core/analyzer/cpp-header-resolver.d.ts.map +1 -0
- package/dist/core/analyzer/cpp-header-resolver.js +71 -0
- package/dist/core/analyzer/cpp-header-resolver.js.map +1 -0
- package/dist/core/analyzer/dependency-graph.d.ts +19 -0
- package/dist/core/analyzer/dependency-graph.d.ts.map +1 -1
- package/dist/core/analyzer/dependency-graph.js +76 -0
- package/dist/core/analyzer/dependency-graph.js.map +1 -1
- package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -1
- package/dist/core/analyzer/duplicate-detector.js +7 -1
- package/dist/core/analyzer/duplicate-detector.js.map +1 -1
- package/dist/core/analyzer/function-registry-trie.d.ts +21 -0
- package/dist/core/analyzer/function-registry-trie.d.ts.map +1 -0
- package/dist/core/analyzer/function-registry-trie.js +39 -0
- package/dist/core/analyzer/function-registry-trie.js.map +1 -0
- package/dist/core/analyzer/import-resolver-bridge.d.ts +25 -0
- package/dist/core/analyzer/import-resolver-bridge.d.ts.map +1 -0
- package/dist/core/analyzer/import-resolver-bridge.js +99 -0
- package/dist/core/analyzer/import-resolver-bridge.js.map +1 -0
- package/dist/core/analyzer/signature-extractor.d.ts.map +1 -1
- package/dist/core/analyzer/signature-extractor.js +131 -3
- package/dist/core/analyzer/signature-extractor.js.map +1 -1
- package/dist/core/analyzer/subgraph-extractor.d.ts +10 -2
- package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -1
- package/dist/core/analyzer/subgraph-extractor.js +25 -7
- package/dist/core/analyzer/subgraph-extractor.js.map +1 -1
- package/dist/core/analyzer/type-inference-engine.d.ts +23 -0
- package/dist/core/analyzer/type-inference-engine.d.ts.map +1 -0
- package/dist/core/analyzer/type-inference-engine.js +130 -0
- package/dist/core/analyzer/type-inference-engine.js.map +1 -0
- package/dist/core/analyzer/vector-index.d.ts +35 -6
- package/dist/core/analyzer/vector-index.d.ts.map +1 -1
- package/dist/core/analyzer/vector-index.js +308 -54
- package/dist/core/analyzer/vector-index.js.map +1 -1
- package/dist/core/generator/spec-pipeline.d.ts +31 -11
- package/dist/core/generator/spec-pipeline.d.ts.map +1 -1
- package/dist/core/generator/spec-pipeline.js +170 -39
- package/dist/core/generator/spec-pipeline.js.map +1 -1
- package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -1
- package/dist/core/generator/stages/stage2-entities.js +2 -1
- package/dist/core/generator/stages/stage2-entities.js.map +1 -1
- package/dist/core/generator/stages/stage3-services.d.ts.map +1 -1
- package/dist/core/generator/stages/stage3-services.js +2 -1
- package/dist/core/generator/stages/stage3-services.js.map +1 -1
- package/dist/core/generator/stages/stage4-api.d.ts.map +1 -1
- package/dist/core/generator/stages/stage4-api.js +2 -1
- package/dist/core/generator/stages/stage4-api.js.map +1 -1
- package/dist/core/generator/stages/stage5-architecture.d.ts +2 -1
- package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -1
- package/dist/core/generator/stages/stage5-architecture.js +15 -3
- package/dist/core/generator/stages/stage5-architecture.js.map +1 -1
- package/dist/core/services/chat-agent.d.ts +5 -0
- package/dist/core/services/chat-agent.d.ts.map +1 -1
- package/dist/core/services/chat-agent.js +14 -0
- package/dist/core/services/chat-agent.js.map +1 -1
- package/dist/core/services/chat-tools.d.ts.map +1 -1
- package/dist/core/services/chat-tools.js +172 -50
- package/dist/core/services/chat-tools.js.map +1 -1
- package/dist/core/services/llm-service.d.ts +23 -1
- package/dist/core/services/llm-service.d.ts.map +1 -1
- package/dist/core/services/llm-service.js +94 -2
- package/dist/core/services/llm-service.js.map +1 -1
- package/dist/core/services/mcp-handlers/analysis.d.ts +12 -0
- package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/analysis.js +138 -2
- package/dist/core/services/mcp-handlers/analysis.js.map +1 -1
- package/dist/core/services/mcp-handlers/graph.d.ts +21 -1
- package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/graph.js +142 -2
- package/dist/core/services/mcp-handlers/graph.js.map +1 -1
- package/dist/core/services/mcp-handlers/orient.d.ts +17 -0
- package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -0
- package/dist/core/services/mcp-handlers/orient.js +200 -0
- package/dist/core/services/mcp-handlers/orient.js.map +1 -0
- package/dist/core/services/mcp-handlers/semantic.d.ts +18 -4
- package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/semantic.js +161 -17
- package/dist/core/services/mcp-handlers/semantic.js.map +1 -1
- package/dist/core/services/mcp-handlers/utils.d.ts +43 -0
- package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/utils.js +66 -1
- package/dist/core/services/mcp-handlers/utils.js.map +1 -1
- package/dist/core/services/mcp-watcher.d.ts +41 -0
- package/dist/core/services/mcp-watcher.d.ts.map +1 -0
- package/dist/core/services/mcp-watcher.js +177 -0
- package/dist/core/services/mcp-watcher.js.map +1 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/pipeline.d.ts +7 -0
- package/dist/types/pipeline.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/viewer/InteractiveGraphViewer.jsx +39 -10
- package/src/viewer/components/ChatPanel.jsx +8 -5
- package/src/viewer/components/ClassGraph.jsx +782 -0
- package/src/viewer/components/FlatGraph.jsx +3 -3
- package/src/viewer/utils/graph-helpers.js +9 -1
- package/src/viewer/utils/themes.js +36 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Call Graph Analyzer
|
|
3
3
|
*
|
|
4
4
|
* Performs static analysis of function calls across source files using tree-sitter.
|
|
5
|
-
* Supports TypeScript/JavaScript, Python, Go, Rust, Ruby, Java — no LLM, pure AST.
|
|
5
|
+
* Supports TypeScript/JavaScript, Python, Go, Rust, Ruby, Java, Swift — no LLM, pure AST.
|
|
6
6
|
*
|
|
7
7
|
* Produces:
|
|
8
8
|
* - FunctionNode[] — all identified functions/methods
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* - Layer violations — cross-layer calls in the wrong direction
|
|
13
13
|
*/
|
|
14
14
|
import Parser from 'tree-sitter';
|
|
15
|
+
import { FunctionRegistryTrie } from './function-registry-trie.js';
|
|
16
|
+
import { inferTypesFromSource, resolveViaTypeInference } from './type-inference-engine.js';
|
|
17
|
+
import { extractAllHttpEdges } from './http-route-parser.js';
|
|
15
18
|
// ============================================================================
|
|
16
19
|
// CONSTANTS
|
|
17
20
|
// ============================================================================
|
|
@@ -46,6 +49,11 @@ const IGNORED_CALLEES = new Set([
|
|
|
46
49
|
'extend', 'attr_accessor', 'attr_reader', 'attr_writer',
|
|
47
50
|
// Java common
|
|
48
51
|
'toString', 'equals', 'hashCode', 'getClass', 'println', 'printf',
|
|
52
|
+
// Swift stdlib / builtins
|
|
53
|
+
'print', 'debugPrint', 'dump', 'fatalError', 'precondition', 'preconditionFailure',
|
|
54
|
+
'assert', 'assertionFailure', 'withUnsafePointer', 'withUnsafeMutablePointer',
|
|
55
|
+
'DispatchQueue', 'main', 'async', 'sync', 'append', 'remove', 'insert', 'contains',
|
|
56
|
+
'map', 'filter', 'reduce', 'forEach', 'compactMap', 'flatMap', 'sorted', 'first', 'last',
|
|
49
57
|
// C++ stdlib / builtins
|
|
50
58
|
'cout', 'cin', 'cerr', 'endl', 'malloc', 'free', 'memcpy', 'memset', 'memcmp',
|
|
51
59
|
'strlen', 'strcpy', 'strcat', 'strcmp', 'sprintf', 'snprintf', 'fprintf',
|
|
@@ -54,6 +62,15 @@ const IGNORED_CALLEES = new Set([
|
|
|
54
62
|
'make_shared', 'make_unique', 'move', 'forward', 'swap',
|
|
55
63
|
'static_cast', 'dynamic_cast', 'reinterpret_cast', 'const_cast',
|
|
56
64
|
]);
|
|
65
|
+
/** Returns true if the name should be skipped as a call target. */
|
|
66
|
+
function isIgnoredCallee(name) {
|
|
67
|
+
if (IGNORED_CALLEES.has(name))
|
|
68
|
+
return true;
|
|
69
|
+
// ALL_CAPS names (3+ chars) are almost certainly C/C++ macros, not functions
|
|
70
|
+
if (/^[A-Z][A-Z0-9_]{2,}$/.test(name))
|
|
71
|
+
return true;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
57
74
|
// ============================================================================
|
|
58
75
|
// PARSER SINGLETONS (lazy init)
|
|
59
76
|
// ============================================================================
|
|
@@ -64,6 +81,7 @@ let _rustParser;
|
|
|
64
81
|
let _rubyParser;
|
|
65
82
|
let _javaParser;
|
|
66
83
|
let _cppParser;
|
|
84
|
+
let _swiftParser;
|
|
67
85
|
let _TsLanguage;
|
|
68
86
|
let _PyLanguage;
|
|
69
87
|
let _GoLanguage;
|
|
@@ -71,6 +89,7 @@ let _RustLanguage;
|
|
|
71
89
|
let _RubyLanguage;
|
|
72
90
|
let _JavaLanguage;
|
|
73
91
|
let _CppLanguage;
|
|
92
|
+
let _SwiftLanguage;
|
|
74
93
|
async function getTSParser() {
|
|
75
94
|
if (!_tsParser) {
|
|
76
95
|
const tsModule = await import('tree-sitter-typescript');
|
|
@@ -134,6 +153,15 @@ async function getCppParser() {
|
|
|
134
153
|
}
|
|
135
154
|
return { parser: _cppParser, lang: _CppLanguage };
|
|
136
155
|
}
|
|
156
|
+
async function getSwiftParser() {
|
|
157
|
+
if (!_swiftParser) {
|
|
158
|
+
const swiftModule = await import('tree-sitter-swift');
|
|
159
|
+
_SwiftLanguage = swiftModule.default;
|
|
160
|
+
_swiftParser = new Parser();
|
|
161
|
+
_swiftParser.setLanguage(_SwiftLanguage);
|
|
162
|
+
}
|
|
163
|
+
return { parser: _swiftParser, lang: _SwiftLanguage };
|
|
164
|
+
}
|
|
137
165
|
// ============================================================================
|
|
138
166
|
// ATTRIBUTION HELPER
|
|
139
167
|
// ============================================================================
|
|
@@ -156,6 +184,200 @@ function findEnclosingFunction(nodes, callPos) {
|
|
|
156
184
|
return best;
|
|
157
185
|
}
|
|
158
186
|
// ============================================================================
|
|
187
|
+
// DOCSTRING / SIGNATURE EXTRACTION HELPERS
|
|
188
|
+
// ============================================================================
|
|
189
|
+
/**
|
|
190
|
+
* Scan backward from `startIndex` in `source` to find the doc comment
|
|
191
|
+
* immediately preceding the function declaration. Skip blank lines.
|
|
192
|
+
*
|
|
193
|
+
* For Python, docstrings are INSIDE the function body — scan forward from
|
|
194
|
+
* `startIndex` past the `def name(...):` colon to find the triple-quoted string.
|
|
195
|
+
*
|
|
196
|
+
* Returns the first meaningful (non-empty, non-decorator) line of the comment.
|
|
197
|
+
*/
|
|
198
|
+
function extractDocstringBefore(source, startIndex, language) {
|
|
199
|
+
// ── Python: scan forward past the colon into the function body ──────────
|
|
200
|
+
if (language === 'Python') {
|
|
201
|
+
// Find the colon that ends the `def` line
|
|
202
|
+
let i = startIndex;
|
|
203
|
+
while (i < source.length && source[i] !== ':')
|
|
204
|
+
i++;
|
|
205
|
+
// Skip past the colon
|
|
206
|
+
i++;
|
|
207
|
+
// Skip whitespace / newline
|
|
208
|
+
while (i < source.length && (source[i] === ' ' || source[i] === '\t' || source[i] === '\n' || source[i] === '\r'))
|
|
209
|
+
i++;
|
|
210
|
+
// Check for triple-quoted docstring
|
|
211
|
+
const tripleDouble = source.startsWith('"""', i);
|
|
212
|
+
const tripleSingle = source.startsWith("'''", i);
|
|
213
|
+
if (tripleDouble || tripleSingle) {
|
|
214
|
+
const quote = tripleDouble ? '"""' : "'''";
|
|
215
|
+
const bodyStart = i + 3;
|
|
216
|
+
const closeIdx = source.indexOf(quote, bodyStart);
|
|
217
|
+
if (closeIdx === -1)
|
|
218
|
+
return undefined;
|
|
219
|
+
const inner = source.slice(bodyStart, closeIdx);
|
|
220
|
+
const firstLine = inner.split('\n').map(l => l.trim()).find(l => l.length > 0);
|
|
221
|
+
return firstLine ?? undefined;
|
|
222
|
+
}
|
|
223
|
+
return undefined;
|
|
224
|
+
}
|
|
225
|
+
// ── All other languages: scan backward from startIndex ─────────────────
|
|
226
|
+
// Move to the character just before startIndex
|
|
227
|
+
let pos = startIndex - 1;
|
|
228
|
+
// Skip trailing whitespace / newlines before the declaration
|
|
229
|
+
while (pos >= 0 && (source[pos] === ' ' || source[pos] === '\t' || source[pos] === '\n' || source[pos] === '\r')) {
|
|
230
|
+
pos--;
|
|
231
|
+
}
|
|
232
|
+
if (pos < 0)
|
|
233
|
+
return undefined;
|
|
234
|
+
// ── TypeScript / JavaScript / Java / C++: JSDoc block /** ... */ ────────
|
|
235
|
+
if (language === 'TypeScript' || language === 'JavaScript' ||
|
|
236
|
+
language === 'Java' || language === 'C++') {
|
|
237
|
+
// Expect closing */ of a JSDoc block
|
|
238
|
+
if (source[pos] === '/' && pos > 0 && source[pos - 1] === '*') {
|
|
239
|
+
const closePos = pos - 1; // points at '*' of closing '*/'
|
|
240
|
+
// Find opening /**
|
|
241
|
+
const openIdx = source.lastIndexOf('/**', closePos);
|
|
242
|
+
if (openIdx === -1)
|
|
243
|
+
return undefined;
|
|
244
|
+
const inner = source.slice(openIdx + 3, closePos - 0);
|
|
245
|
+
// Remove leading * on each line, find first non-empty, non-@ line
|
|
246
|
+
const firstLine = inner
|
|
247
|
+
.split('\n')
|
|
248
|
+
.map(l => l.replace(/^\s*\*\s?/, '').trim())
|
|
249
|
+
.find(l => l.length > 0 && !l.startsWith('@'));
|
|
250
|
+
return firstLine ?? undefined;
|
|
251
|
+
}
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
// ── Go: // comment lines immediately before ──────────────────────────────
|
|
255
|
+
if (language === 'Go') {
|
|
256
|
+
const lines = [];
|
|
257
|
+
// Walk backward line by line
|
|
258
|
+
let lineEnd = pos;
|
|
259
|
+
while (lineEnd >= 0) {
|
|
260
|
+
// Find start of this line
|
|
261
|
+
let lineStart = lineEnd;
|
|
262
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n')
|
|
263
|
+
lineStart--;
|
|
264
|
+
const line = source.slice(lineStart, lineEnd + 1).trimEnd();
|
|
265
|
+
const trimmed = line.trim();
|
|
266
|
+
if (trimmed.startsWith('//')) {
|
|
267
|
+
lines.unshift(trimmed.slice(2).trim());
|
|
268
|
+
lineEnd = lineStart - 1;
|
|
269
|
+
// Skip over the newline
|
|
270
|
+
while (lineEnd >= 0 && (source[lineEnd] === '\n' || source[lineEnd] === '\r'))
|
|
271
|
+
lineEnd--;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return lines.find(l => l.length > 0) ?? undefined;
|
|
278
|
+
}
|
|
279
|
+
// ── Rust / Swift: /// doc comment lines immediately before ─────────────
|
|
280
|
+
if (language === 'Rust' || language === 'Swift') {
|
|
281
|
+
const lines = [];
|
|
282
|
+
let lineEnd = pos;
|
|
283
|
+
while (lineEnd >= 0) {
|
|
284
|
+
let lineStart = lineEnd;
|
|
285
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n')
|
|
286
|
+
lineStart--;
|
|
287
|
+
const line = source.slice(lineStart, lineEnd + 1).trimEnd();
|
|
288
|
+
const trimmed = line.trim();
|
|
289
|
+
if (trimmed.startsWith('///')) {
|
|
290
|
+
lines.unshift(trimmed.slice(3).trim());
|
|
291
|
+
lineEnd = lineStart - 1;
|
|
292
|
+
while (lineEnd >= 0 && (source[lineEnd] === '\n' || source[lineEnd] === '\r'))
|
|
293
|
+
lineEnd--;
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return lines.find(l => l.length > 0) ?? undefined;
|
|
300
|
+
}
|
|
301
|
+
// ── Ruby: # comment lines immediately before ─────────────────────────────
|
|
302
|
+
if (language === 'Ruby') {
|
|
303
|
+
const lines = [];
|
|
304
|
+
let lineEnd = pos;
|
|
305
|
+
while (lineEnd >= 0) {
|
|
306
|
+
let lineStart = lineEnd;
|
|
307
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n')
|
|
308
|
+
lineStart--;
|
|
309
|
+
const line = source.slice(lineStart, lineEnd + 1).trimEnd();
|
|
310
|
+
const trimmed = line.trim();
|
|
311
|
+
if (trimmed.startsWith('#')) {
|
|
312
|
+
lines.unshift(trimmed.slice(1).trim());
|
|
313
|
+
lineEnd = lineStart - 1;
|
|
314
|
+
while (lineEnd >= 0 && (source[lineEnd] === '\n' || source[lineEnd] === '\r'))
|
|
315
|
+
lineEnd--;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return lines.find(l => l.length > 0) ?? undefined;
|
|
322
|
+
}
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Extract the function declaration (signature without body) from
|
|
327
|
+
* `source.slice(startIndex, endIndex)`.
|
|
328
|
+
*
|
|
329
|
+
* Strategy:
|
|
330
|
+
* - TS/JS/Java/C++/Go/Rust/Ruby: take everything up to the first `{` at depth 0
|
|
331
|
+
* - Python: take everything up to the first `:` that ends the `def` line
|
|
332
|
+
*
|
|
333
|
+
* Whitespace is normalized (multiple spaces/newlines → single space).
|
|
334
|
+
* Limited to 300 characters max.
|
|
335
|
+
*/
|
|
336
|
+
function extractDeclaration(source, startIndex, endIndex, language) {
|
|
337
|
+
const slice = source.slice(startIndex, Math.min(endIndex, startIndex + 1500));
|
|
338
|
+
let decl;
|
|
339
|
+
if (language === 'Python') {
|
|
340
|
+
// Take up to (not including) the first `:` that ends the def line
|
|
341
|
+
// We scan for `:` while tracking parenthesis depth to avoid matching
|
|
342
|
+
// colons inside type annotations (e.g., def f(x: int) -> dict[str, int]:)
|
|
343
|
+
let depth = 0;
|
|
344
|
+
let end = -1;
|
|
345
|
+
for (let i = 0; i < slice.length; i++) {
|
|
346
|
+
const ch = slice[i];
|
|
347
|
+
if (ch === '(' || ch === '[' || ch === '{')
|
|
348
|
+
depth++;
|
|
349
|
+
else if (ch === ')' || ch === ']' || ch === '}')
|
|
350
|
+
depth--;
|
|
351
|
+
else if (ch === ':' && depth === 0) {
|
|
352
|
+
end = i;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
decl = end !== -1 ? slice.slice(0, end) : slice.slice(0, 300);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
// Find first `{` at brace depth 0
|
|
360
|
+
let depth = 0;
|
|
361
|
+
let end = -1;
|
|
362
|
+
for (let i = 0; i < slice.length; i++) {
|
|
363
|
+
const ch = slice[i];
|
|
364
|
+
if (ch === '{') {
|
|
365
|
+
if (depth === 0) {
|
|
366
|
+
end = i;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
depth++;
|
|
370
|
+
}
|
|
371
|
+
else if (ch === '}') {
|
|
372
|
+
depth--;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
decl = end !== -1 ? slice.slice(0, end) : slice.slice(0, 300);
|
|
376
|
+
}
|
|
377
|
+
// Normalize whitespace
|
|
378
|
+
return decl.replace(/\s+/g, ' ').trim().slice(0, 300);
|
|
379
|
+
}
|
|
380
|
+
// ============================================================================
|
|
159
381
|
// TYPESCRIPT EXTRACTOR
|
|
160
382
|
// ============================================================================
|
|
161
383
|
const TS_FN_QUERY = `
|
|
@@ -178,6 +400,7 @@ const TS_CALL_QUERY = `
|
|
|
178
400
|
(call_expression
|
|
179
401
|
function: [(identifier) @call.name
|
|
180
402
|
(member_expression
|
|
403
|
+
object: (identifier) @call.object
|
|
181
404
|
property: (property_identifier) @call.name)]) @call.node
|
|
182
405
|
`;
|
|
183
406
|
async function extractTSGraph(filePath, content) {
|
|
@@ -224,6 +447,8 @@ async function extractTSGraph(filePath, content) {
|
|
|
224
447
|
endIndex: fnNode.endIndex,
|
|
225
448
|
fanIn: 0,
|
|
226
449
|
fanOut: 0,
|
|
450
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'TypeScript'),
|
|
451
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'TypeScript'),
|
|
227
452
|
});
|
|
228
453
|
}
|
|
229
454
|
// --- Extract calls ---
|
|
@@ -232,10 +457,11 @@ async function extractTSGraph(filePath, content) {
|
|
|
232
457
|
for (const match of callMatches) {
|
|
233
458
|
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
234
459
|
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
460
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
235
461
|
if (!nameCapture || !nodeCapture)
|
|
236
462
|
continue;
|
|
237
463
|
const calleeName = nameCapture.node.text;
|
|
238
|
-
if (
|
|
464
|
+
if (isIgnoredCallee(calleeName))
|
|
239
465
|
continue;
|
|
240
466
|
const callPos = nodeCapture.node.startIndex;
|
|
241
467
|
const caller = findEnclosingFunction(nodes, callPos);
|
|
@@ -245,6 +471,7 @@ async function extractTSGraph(filePath, content) {
|
|
|
245
471
|
callerId: caller.id,
|
|
246
472
|
calleeName,
|
|
247
473
|
line: nodeCapture.node.startPosition.row + 1,
|
|
474
|
+
calleeObject: objectCapture?.node.text,
|
|
248
475
|
});
|
|
249
476
|
}
|
|
250
477
|
return { nodes, rawEdges };
|
|
@@ -331,6 +558,8 @@ async function extractPyGraph(filePath, content) {
|
|
|
331
558
|
endIndex: fnNode.endIndex,
|
|
332
559
|
fanIn: 0,
|
|
333
560
|
fanOut: 0,
|
|
561
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'Python'),
|
|
562
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Python'),
|
|
334
563
|
});
|
|
335
564
|
}
|
|
336
565
|
// --- Extract calls ---
|
|
@@ -344,7 +573,7 @@ async function extractPyGraph(filePath, content) {
|
|
|
344
573
|
if (!nameCapture || !nodeCapture)
|
|
345
574
|
continue;
|
|
346
575
|
const calleeName = nameCapture.node.text;
|
|
347
|
-
if (
|
|
576
|
+
if (isIgnoredCallee(calleeName))
|
|
348
577
|
continue;
|
|
349
578
|
const callPos = nodeCapture.node.startIndex;
|
|
350
579
|
const caller = findEnclosingFunction(nodes, callPos);
|
|
@@ -356,20 +585,15 @@ async function extractPyGraph(filePath, content) {
|
|
|
356
585
|
line: nodeCapture.node.startPosition.row + 1,
|
|
357
586
|
});
|
|
358
587
|
}
|
|
359
|
-
// Method calls: obj.method() —
|
|
588
|
+
// Method calls: obj.method() — capture receiver for type-inference-based resolution
|
|
360
589
|
for (const match of methodCallQuery.matches(tree.rootNode)) {
|
|
361
590
|
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
362
591
|
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
363
592
|
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
364
593
|
if (!objectCapture || !nameCapture || !nodeCapture)
|
|
365
594
|
continue;
|
|
366
|
-
const objectName = objectCapture.node.text;
|
|
367
|
-
// Only track self.method() and cls.method() — external objects like
|
|
368
|
-
// redis.get(), dict.get(), os.path.join() would create massive false positives
|
|
369
|
-
if (objectName !== 'self' && objectName !== 'cls')
|
|
370
|
-
continue;
|
|
371
595
|
const calleeName = nameCapture.node.text;
|
|
372
|
-
if (
|
|
596
|
+
if (isIgnoredCallee(calleeName))
|
|
373
597
|
continue;
|
|
374
598
|
const callPos = nodeCapture.node.startIndex;
|
|
375
599
|
const caller = findEnclosingFunction(nodes, callPos);
|
|
@@ -379,6 +603,7 @@ async function extractPyGraph(filePath, content) {
|
|
|
379
603
|
callerId: caller.id,
|
|
380
604
|
calleeName,
|
|
381
605
|
line: nodeCapture.node.startPosition.row + 1,
|
|
606
|
+
calleeObject: objectCapture.node.text,
|
|
382
607
|
});
|
|
383
608
|
}
|
|
384
609
|
return { nodes, rawEdges };
|
|
@@ -399,6 +624,7 @@ const GO_CALL_QUERY = `
|
|
|
399
624
|
|
|
400
625
|
(call_expression
|
|
401
626
|
function: (selector_expression
|
|
627
|
+
operand: (identifier) @call.object
|
|
402
628
|
field: (field_identifier) @call.name)) @call.node
|
|
403
629
|
`;
|
|
404
630
|
async function extractGoGraph(filePath, content) {
|
|
@@ -434,21 +660,24 @@ async function extractGoGraph(filePath, content) {
|
|
|
434
660
|
startIndex: fnNode.startIndex,
|
|
435
661
|
endIndex: fnNode.endIndex,
|
|
436
662
|
fanIn: 0, fanOut: 0,
|
|
663
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'Go'),
|
|
664
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Go'),
|
|
437
665
|
});
|
|
438
666
|
}
|
|
439
667
|
const rawEdges = [];
|
|
440
668
|
for (const match of callQuery.matches(tree.rootNode)) {
|
|
441
669
|
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
442
670
|
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
671
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
443
672
|
if (!nameCapture || !nodeCapture)
|
|
444
673
|
continue;
|
|
445
674
|
const calleeName = nameCapture.node.text;
|
|
446
|
-
if (
|
|
675
|
+
if (isIgnoredCallee(calleeName))
|
|
447
676
|
continue;
|
|
448
677
|
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
449
678
|
if (!caller)
|
|
450
679
|
continue;
|
|
451
|
-
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
|
|
680
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
|
|
452
681
|
}
|
|
453
682
|
return { nodes, rawEdges };
|
|
454
683
|
}
|
|
@@ -465,6 +694,7 @@ const RUST_CALL_QUERY = `
|
|
|
465
694
|
|
|
466
695
|
(call_expression
|
|
467
696
|
function: (field_expression
|
|
697
|
+
value: (identifier) @call.object
|
|
468
698
|
field: (field_identifier) @call.name)) @call.node
|
|
469
699
|
`;
|
|
470
700
|
async function extractRustGraph(filePath, content) {
|
|
@@ -502,21 +732,24 @@ async function extractRustGraph(filePath, content) {
|
|
|
502
732
|
startIndex: fnNode.startIndex,
|
|
503
733
|
endIndex: fnNode.endIndex,
|
|
504
734
|
fanIn: 0, fanOut: 0,
|
|
735
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'Rust'),
|
|
736
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Rust'),
|
|
505
737
|
});
|
|
506
738
|
}
|
|
507
739
|
const rawEdges = [];
|
|
508
740
|
for (const match of callQuery.matches(tree.rootNode)) {
|
|
509
741
|
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
510
742
|
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
743
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
511
744
|
if (!nameCapture || !nodeCapture)
|
|
512
745
|
continue;
|
|
513
746
|
const calleeName = nameCapture.node.text;
|
|
514
|
-
if (
|
|
747
|
+
if (isIgnoredCallee(calleeName))
|
|
515
748
|
continue;
|
|
516
749
|
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
517
750
|
if (!caller)
|
|
518
751
|
continue;
|
|
519
|
-
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
|
|
752
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
|
|
520
753
|
}
|
|
521
754
|
return { nodes, rawEdges };
|
|
522
755
|
}
|
|
@@ -530,8 +763,12 @@ const RUBY_FN_QUERY = `
|
|
|
530
763
|
(singleton_method
|
|
531
764
|
name: (identifier) @fn.name) @fn.node
|
|
532
765
|
`;
|
|
533
|
-
// Explicit calls:
|
|
766
|
+
// Explicit calls: fn(), obj.method()
|
|
534
767
|
const RUBY_CALL_QUERY = `
|
|
768
|
+
(call
|
|
769
|
+
receiver: (identifier) @call.object
|
|
770
|
+
method: (identifier) @call.name) @call.node
|
|
771
|
+
|
|
535
772
|
(call
|
|
536
773
|
method: (identifier) @call.name) @call.node
|
|
537
774
|
`;
|
|
@@ -576,30 +813,33 @@ async function extractRubyGraph(filePath, content) {
|
|
|
576
813
|
startIndex: fnNode.startIndex,
|
|
577
814
|
endIndex: fnNode.endIndex,
|
|
578
815
|
fanIn: 0, fanOut: 0,
|
|
816
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'Ruby'),
|
|
817
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Ruby'),
|
|
579
818
|
});
|
|
580
819
|
}
|
|
581
820
|
const rawEdges = [];
|
|
582
|
-
// Explicit calls:
|
|
821
|
+
// Explicit calls: fn(), obj.method()
|
|
583
822
|
for (const match of callQuery.matches(tree.rootNode)) {
|
|
584
823
|
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
585
824
|
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
825
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
586
826
|
if (!nameCapture || !nodeCapture)
|
|
587
827
|
continue;
|
|
588
828
|
const calleeName = nameCapture.node.text;
|
|
589
|
-
if (
|
|
829
|
+
if (isIgnoredCallee(calleeName))
|
|
590
830
|
continue;
|
|
591
831
|
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
592
832
|
if (!caller)
|
|
593
833
|
continue;
|
|
594
|
-
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
|
|
834
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
|
|
595
835
|
}
|
|
596
|
-
// Bareword calls:
|
|
836
|
+
// Bareword calls: identifier at statement level, no parens
|
|
597
837
|
for (const match of barewordQuery.matches(tree.rootNode)) {
|
|
598
838
|
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
599
839
|
if (!nameCapture)
|
|
600
840
|
continue;
|
|
601
841
|
const calleeName = nameCapture.node.text;
|
|
602
|
-
if (
|
|
842
|
+
if (isIgnoredCallee(calleeName))
|
|
603
843
|
continue;
|
|
604
844
|
const caller = findEnclosingFunction(nodes, nameCapture.node.startIndex);
|
|
605
845
|
if (!caller)
|
|
@@ -619,6 +859,10 @@ const JAVA_FN_QUERY = `
|
|
|
619
859
|
name: (identifier) @fn.name) @fn.node
|
|
620
860
|
`;
|
|
621
861
|
const JAVA_CALL_QUERY = `
|
|
862
|
+
(method_invocation
|
|
863
|
+
object: (identifier) @call.object
|
|
864
|
+
name: (identifier) @call.name) @call.node
|
|
865
|
+
|
|
622
866
|
(method_invocation
|
|
623
867
|
name: (identifier) @call.name) @call.node
|
|
624
868
|
`;
|
|
@@ -656,21 +900,24 @@ async function extractJavaGraph(filePath, content) {
|
|
|
656
900
|
startIndex: fnNode.startIndex,
|
|
657
901
|
endIndex: fnNode.endIndex,
|
|
658
902
|
fanIn: 0, fanOut: 0,
|
|
903
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'Java'),
|
|
904
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Java'),
|
|
659
905
|
});
|
|
660
906
|
}
|
|
661
907
|
const rawEdges = [];
|
|
662
908
|
for (const match of callQuery.matches(tree.rootNode)) {
|
|
663
909
|
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
664
910
|
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
911
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
665
912
|
if (!nameCapture || !nodeCapture)
|
|
666
913
|
continue;
|
|
667
914
|
const calleeName = nameCapture.node.text;
|
|
668
|
-
if (
|
|
915
|
+
if (isIgnoredCallee(calleeName))
|
|
669
916
|
continue;
|
|
670
917
|
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
671
918
|
if (!caller)
|
|
672
919
|
continue;
|
|
673
|
-
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
|
|
920
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
|
|
674
921
|
}
|
|
675
922
|
return { nodes, rawEdges };
|
|
676
923
|
}
|
|
@@ -708,13 +955,16 @@ const CPP_FN_QUALIFIED_QUERY = `
|
|
|
708
955
|
declarator: (qualified_identifier
|
|
709
956
|
name: (identifier) @fn.name))) @fn.node
|
|
710
957
|
`;
|
|
711
|
-
/**
|
|
712
|
-
const
|
|
958
|
+
/** Plain function calls: foo() */
|
|
959
|
+
const CPP_CALL_DIRECT_QUERY = `
|
|
713
960
|
(call_expression
|
|
714
961
|
function: (identifier) @call.name) @call.node
|
|
715
|
-
|
|
962
|
+
`;
|
|
963
|
+
/** Member calls: obj.method() and ptr->method() — captures receiver */
|
|
964
|
+
const CPP_CALL_MEMBER_QUERY = `
|
|
716
965
|
(call_expression
|
|
717
966
|
function: (field_expression
|
|
967
|
+
argument: (identifier) @call.object
|
|
718
968
|
field: (field_identifier) @call.name)) @call.node
|
|
719
969
|
`;
|
|
720
970
|
async function extractCppGraph(filePath, content) {
|
|
@@ -732,6 +982,9 @@ async function extractCppGraph(filePath, content) {
|
|
|
732
982
|
continue;
|
|
733
983
|
seen.add(nameCapture.node.startIndex);
|
|
734
984
|
const name = nameCapture.node.text;
|
|
985
|
+
// Skip ALL_CAPS names — these are almost certainly macros, not functions
|
|
986
|
+
if (/^[A-Z][A-Z0-9_]{2,}$/.test(name))
|
|
987
|
+
continue;
|
|
735
988
|
const fnNode = nodeCapture.node;
|
|
736
989
|
// Find enclosing class (inline method defined inside class body)
|
|
737
990
|
let className;
|
|
@@ -765,37 +1018,392 @@ async function extractCppGraph(filePath, content) {
|
|
|
765
1018
|
startIndex: fnNode.startIndex,
|
|
766
1019
|
endIndex: fnNode.endIndex,
|
|
767
1020
|
fanIn: 0, fanOut: 0,
|
|
1021
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'C++'),
|
|
1022
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'C++'),
|
|
768
1023
|
});
|
|
769
1024
|
}
|
|
770
1025
|
}
|
|
771
1026
|
const rawEdges = [];
|
|
772
|
-
|
|
1027
|
+
// Plain calls: foo()
|
|
1028
|
+
for (const match of safeQuery(lang, CPP_CALL_DIRECT_QUERY, tree.rootNode)) {
|
|
773
1029
|
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
774
1030
|
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
775
1031
|
if (!nameCapture || !nodeCapture)
|
|
776
1032
|
continue;
|
|
777
1033
|
const calleeName = nameCapture.node.text;
|
|
778
|
-
if (
|
|
1034
|
+
if (isIgnoredCallee(calleeName))
|
|
779
1035
|
continue;
|
|
780
1036
|
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
781
1037
|
if (!caller)
|
|
782
1038
|
continue;
|
|
783
1039
|
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
|
|
784
1040
|
}
|
|
1041
|
+
// Member calls: obj.method() / ptr->method()
|
|
1042
|
+
for (const match of safeQuery(lang, CPP_CALL_MEMBER_QUERY, tree.rootNode)) {
|
|
1043
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
1044
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
1045
|
+
const objectCapture = match.captures.find(c => c.name === 'call.object');
|
|
1046
|
+
if (!nameCapture || !nodeCapture)
|
|
1047
|
+
continue;
|
|
1048
|
+
const calleeName = nameCapture.node.text;
|
|
1049
|
+
if (isIgnoredCallee(calleeName))
|
|
1050
|
+
continue;
|
|
1051
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
1052
|
+
if (!caller)
|
|
1053
|
+
continue;
|
|
1054
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
|
|
1055
|
+
}
|
|
785
1056
|
return { nodes, rawEdges };
|
|
786
1057
|
}
|
|
787
1058
|
// ============================================================================
|
|
1059
|
+
// SWIFT EXTRACTOR
|
|
1060
|
+
// ============================================================================
|
|
1061
|
+
// function_declaration covers free functions and methods inside class_body
|
|
1062
|
+
const SWIFT_FN_QUERY = `
|
|
1063
|
+
(function_declaration
|
|
1064
|
+
name: (simple_identifier) @fn.name) @fn.node
|
|
1065
|
+
|
|
1066
|
+
(init_declaration) @fn.node
|
|
1067
|
+
`;
|
|
1068
|
+
// Direct calls: foo()
|
|
1069
|
+
const SWIFT_CALL_DIRECT_QUERY = `
|
|
1070
|
+
(call_expression
|
|
1071
|
+
(simple_identifier) @call.name) @call.node
|
|
1072
|
+
`;
|
|
1073
|
+
// Method calls: obj.method() / self.method()
|
|
1074
|
+
const SWIFT_CALL_NAV_QUERY = `
|
|
1075
|
+
(call_expression
|
|
1076
|
+
(navigation_expression
|
|
1077
|
+
(navigation_suffix
|
|
1078
|
+
(simple_identifier) @call.name))) @call.node
|
|
1079
|
+
`;
|
|
1080
|
+
async function extractSwiftGraph(filePath, content) {
|
|
1081
|
+
const { parser, lang } = await getSwiftParser();
|
|
1082
|
+
const tree = parser.parse(content);
|
|
1083
|
+
const fnQuery = new Parser.Query(lang, SWIFT_FN_QUERY);
|
|
1084
|
+
const directCallQuery = new Parser.Query(lang, SWIFT_CALL_DIRECT_QUERY);
|
|
1085
|
+
const navCallQuery = new Parser.Query(lang, SWIFT_CALL_NAV_QUERY);
|
|
1086
|
+
const nodes = [];
|
|
1087
|
+
for (const match of fnQuery.matches(tree.rootNode)) {
|
|
1088
|
+
const nameCapture = match.captures.find(c => c.name === 'fn.name');
|
|
1089
|
+
const nodeCapture = match.captures.find(c => c.name === 'fn.node');
|
|
1090
|
+
if (!nodeCapture)
|
|
1091
|
+
continue;
|
|
1092
|
+
const fnNode = nodeCapture.node;
|
|
1093
|
+
const name = nameCapture?.node.text ?? 'init';
|
|
1094
|
+
// Find enclosing class/struct/actor/enum/extension (all are class_declaration in this grammar)
|
|
1095
|
+
let className;
|
|
1096
|
+
let cursor = fnNode.parent;
|
|
1097
|
+
while (cursor) {
|
|
1098
|
+
if (cursor.type === 'class_declaration') {
|
|
1099
|
+
const nameNode = cursor.children.find(c => c.type === 'type_identifier');
|
|
1100
|
+
if (nameNode)
|
|
1101
|
+
className = nameNode.text;
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
cursor = cursor.parent;
|
|
1105
|
+
}
|
|
1106
|
+
const isAsync = content.slice(fnNode.startIndex, fnNode.endIndex).includes(' async ');
|
|
1107
|
+
const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
|
|
1108
|
+
nodes.push({
|
|
1109
|
+
id, name, filePath, className,
|
|
1110
|
+
isAsync,
|
|
1111
|
+
language: 'Swift',
|
|
1112
|
+
startIndex: fnNode.startIndex,
|
|
1113
|
+
endIndex: fnNode.endIndex,
|
|
1114
|
+
fanIn: 0, fanOut: 0,
|
|
1115
|
+
docstring: extractDocstringBefore(content, fnNode.startIndex, 'Swift'),
|
|
1116
|
+
signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Swift'),
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
const rawEdges = [];
|
|
1120
|
+
// Direct calls: foo()
|
|
1121
|
+
for (const match of directCallQuery.matches(tree.rootNode)) {
|
|
1122
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
1123
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
1124
|
+
if (!nameCapture || !nodeCapture)
|
|
1125
|
+
continue;
|
|
1126
|
+
const calleeName = nameCapture.node.text;
|
|
1127
|
+
if (isIgnoredCallee(calleeName))
|
|
1128
|
+
continue;
|
|
1129
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
1130
|
+
if (!caller)
|
|
1131
|
+
continue;
|
|
1132
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
|
|
1133
|
+
}
|
|
1134
|
+
// Method calls: obj.method() / self.method()
|
|
1135
|
+
for (const match of navCallQuery.matches(tree.rootNode)) {
|
|
1136
|
+
const nameCapture = match.captures.find(c => c.name === 'call.name');
|
|
1137
|
+
const nodeCapture = match.captures.find(c => c.name === 'call.node');
|
|
1138
|
+
if (!nameCapture || !nodeCapture)
|
|
1139
|
+
continue;
|
|
1140
|
+
const calleeName = nameCapture.node.text;
|
|
1141
|
+
if (isIgnoredCallee(calleeName))
|
|
1142
|
+
continue;
|
|
1143
|
+
const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
|
|
1144
|
+
if (!caller)
|
|
1145
|
+
continue;
|
|
1146
|
+
// Extract the receiver object (first child of navigation_expression)
|
|
1147
|
+
const navExpr = nodeCapture.node.firstChild;
|
|
1148
|
+
const objText = navExpr?.firstChild?.type === 'self_expression'
|
|
1149
|
+
? 'self'
|
|
1150
|
+
: navExpr?.firstChild?.text;
|
|
1151
|
+
rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objText });
|
|
1152
|
+
}
|
|
1153
|
+
return { nodes, rawEdges };
|
|
1154
|
+
}
|
|
1155
|
+
// ============================================================================
|
|
1156
|
+
// CLASS HIERARCHY EXTRACTION
|
|
1157
|
+
// ============================================================================
|
|
1158
|
+
/**
|
|
1159
|
+
* Extract parent class / interface relationships from source files using
|
|
1160
|
+
* tree-sitter. Returns a map from `filePath::ClassName` → relationship info.
|
|
1161
|
+
* Uses safeQuery so any query that doesn't match a grammar version is silently
|
|
1162
|
+
* skipped rather than crashing.
|
|
1163
|
+
*/
|
|
1164
|
+
async function extractClassRelationships(files) {
|
|
1165
|
+
const out = new Map();
|
|
1166
|
+
// Helper to merge into map keyed by `filePath::ClassName`
|
|
1167
|
+
function merge(filePath, className, parents, ifaces) {
|
|
1168
|
+
const key = `${filePath}::${className}`;
|
|
1169
|
+
const existing = out.get(key) ?? { parentClasses: [], interfaces: [] };
|
|
1170
|
+
for (const p of parents)
|
|
1171
|
+
if (!existing.parentClasses.includes(p))
|
|
1172
|
+
existing.parentClasses.push(p);
|
|
1173
|
+
for (const i of ifaces)
|
|
1174
|
+
if (!existing.interfaces.includes(i))
|
|
1175
|
+
existing.interfaces.push(i);
|
|
1176
|
+
out.set(key, existing);
|
|
1177
|
+
}
|
|
1178
|
+
for (const file of files) {
|
|
1179
|
+
try {
|
|
1180
|
+
if (file.language === 'TypeScript' || file.language === 'JavaScript') {
|
|
1181
|
+
const { parser, lang } = await getTSParser();
|
|
1182
|
+
const tree = parser.parse(file.content);
|
|
1183
|
+
// class Foo extends Bar implements Baz, Qux
|
|
1184
|
+
const EXTENDS_Q = `
|
|
1185
|
+
(class_declaration
|
|
1186
|
+
name: (type_identifier) @cls
|
|
1187
|
+
(class_heritage (extends_clause value: (identifier) @parent)))`;
|
|
1188
|
+
const IMPLEMENTS_Q = `
|
|
1189
|
+
(class_declaration
|
|
1190
|
+
name: (type_identifier) @cls
|
|
1191
|
+
(class_heritage (implements_clause (type_identifier) @iface)))`;
|
|
1192
|
+
for (const m of safeQuery(lang, EXTENDS_Q, tree.rootNode)) {
|
|
1193
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1194
|
+
const parent = m.captures.find(c => c.name === 'parent')?.node.text;
|
|
1195
|
+
if (cls && parent)
|
|
1196
|
+
merge(file.path, cls, [parent], []);
|
|
1197
|
+
}
|
|
1198
|
+
for (const m of safeQuery(lang, IMPLEMENTS_Q, tree.rootNode)) {
|
|
1199
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1200
|
+
const iface = m.captures.find(c => c.name === 'iface')?.node.text;
|
|
1201
|
+
if (cls && iface)
|
|
1202
|
+
merge(file.path, cls, [], [iface]);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
else if (file.language === 'Python') {
|
|
1206
|
+
const { parser, lang } = await getPyParser();
|
|
1207
|
+
const tree = parser.parse(file.content);
|
|
1208
|
+
// class Foo(Bar, Baz):
|
|
1209
|
+
const Q = `
|
|
1210
|
+
(class_definition
|
|
1211
|
+
name: (identifier) @cls
|
|
1212
|
+
superclasses: (argument_list (identifier) @parent))`;
|
|
1213
|
+
for (const m of safeQuery(lang, Q, tree.rootNode)) {
|
|
1214
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1215
|
+
const parent = m.captures.find(c => c.name === 'parent')?.node.text;
|
|
1216
|
+
if (cls && parent && parent !== 'object')
|
|
1217
|
+
merge(file.path, cls, [parent], []);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
else if (file.language === 'Java') {
|
|
1221
|
+
const { parser, lang } = await getJavaParser();
|
|
1222
|
+
const tree = parser.parse(file.content);
|
|
1223
|
+
const EXTENDS_Q = `
|
|
1224
|
+
(class_declaration
|
|
1225
|
+
name: (identifier) @cls
|
|
1226
|
+
(superclass (type_identifier) @parent))`;
|
|
1227
|
+
const IMPLEMENTS_Q = `
|
|
1228
|
+
(class_declaration
|
|
1229
|
+
name: (identifier) @cls
|
|
1230
|
+
(super_interfaces (type_list (type_identifier) @iface)))`;
|
|
1231
|
+
for (const m of safeQuery(lang, EXTENDS_Q, tree.rootNode)) {
|
|
1232
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1233
|
+
const parent = m.captures.find(c => c.name === 'parent')?.node.text;
|
|
1234
|
+
if (cls && parent)
|
|
1235
|
+
merge(file.path, cls, [parent], []);
|
|
1236
|
+
}
|
|
1237
|
+
for (const m of safeQuery(lang, IMPLEMENTS_Q, tree.rootNode)) {
|
|
1238
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1239
|
+
const iface = m.captures.find(c => c.name === 'iface')?.node.text;
|
|
1240
|
+
if (cls && iface)
|
|
1241
|
+
merge(file.path, cls, [], [iface]);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
else if (file.language === 'C++') {
|
|
1245
|
+
const { parser, lang } = await getCppParser();
|
|
1246
|
+
const tree = parser.parse(file.content);
|
|
1247
|
+
// class Foo : public Bar
|
|
1248
|
+
const Q = `
|
|
1249
|
+
(class_specifier
|
|
1250
|
+
name: (type_identifier) @cls
|
|
1251
|
+
(base_class_clause (type_identifier) @parent))`;
|
|
1252
|
+
for (const m of safeQuery(lang, Q, tree.rootNode)) {
|
|
1253
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1254
|
+
const parent = m.captures.find(c => c.name === 'parent')?.node.text;
|
|
1255
|
+
if (cls && parent)
|
|
1256
|
+
merge(file.path, cls, [parent], []);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
else if (file.language === 'Ruby') {
|
|
1260
|
+
const { parser, lang } = await getRubyParser();
|
|
1261
|
+
const tree = parser.parse(file.content);
|
|
1262
|
+
// class Foo < Bar
|
|
1263
|
+
const Q = `
|
|
1264
|
+
(class
|
|
1265
|
+
name: (constant) @cls
|
|
1266
|
+
superclass: (superclass (constant) @parent))`;
|
|
1267
|
+
for (const m of safeQuery(lang, Q, tree.rootNode)) {
|
|
1268
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1269
|
+
const parent = m.captures.find(c => c.name === 'parent')?.node.text;
|
|
1270
|
+
if (cls && parent)
|
|
1271
|
+
merge(file.path, cls, [parent], []);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
else if (file.language === 'Go') {
|
|
1275
|
+
// Go has no inheritance but has struct embedding; treat as 'embeds' edges
|
|
1276
|
+
const { parser, lang } = await getGoParser();
|
|
1277
|
+
const tree = parser.parse(file.content);
|
|
1278
|
+
// Anonymous (embedded) field in a struct: type Foo struct { Bar }
|
|
1279
|
+
const Q = `
|
|
1280
|
+
(type_declaration
|
|
1281
|
+
(type_spec
|
|
1282
|
+
name: (type_identifier) @cls
|
|
1283
|
+
type: (struct_type
|
|
1284
|
+
(field_declaration_list
|
|
1285
|
+
(field_declaration
|
|
1286
|
+
type: (type_identifier) @embedded)))))`;
|
|
1287
|
+
for (const m of safeQuery(lang, Q, tree.rootNode)) {
|
|
1288
|
+
const cls = m.captures.find(c => c.name === 'cls')?.node.text;
|
|
1289
|
+
const embedded = m.captures.find(c => c.name === 'embedded')?.node.text;
|
|
1290
|
+
if (cls && embedded) {
|
|
1291
|
+
const key = `${file.path}::${cls}`;
|
|
1292
|
+
const existing = out.get(key) ?? { parentClasses: [], interfaces: [] };
|
|
1293
|
+
// Store Go embeds as parentClasses (will be tagged as 'embeds' when building edges)
|
|
1294
|
+
if (!existing.parentClasses.includes(embedded))
|
|
1295
|
+
existing.parentClasses.push(embedded);
|
|
1296
|
+
out.set(key, existing);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
// Rust: trait impls are structural but less like OOP inheritance; skip for now
|
|
1301
|
+
}
|
|
1302
|
+
catch {
|
|
1303
|
+
// Best-effort; skip unparseable files
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return out;
|
|
1307
|
+
}
|
|
1308
|
+
/**
|
|
1309
|
+
* Build ClassNode[] from the set of extracted FunctionNodes (which carry
|
|
1310
|
+
* `className`), enriched with inheritance data from `extractClassRelationships`.
|
|
1311
|
+
*
|
|
1312
|
+
* Functions without a className are grouped by file into synthetic module nodes
|
|
1313
|
+
* (e.g. `[call-graph]`) so every function appears in the class graph, not just
|
|
1314
|
+
* class methods. This is essential for codebases that use mostly module-level
|
|
1315
|
+
* exports rather than OOP classes.
|
|
1316
|
+
*/
|
|
1317
|
+
function buildClassNodes(allNodes, relationships) {
|
|
1318
|
+
// Group FunctionNodes by (filePath, className).
|
|
1319
|
+
// Free functions use a synthetic "[basename]" module name keyed by filePath alone.
|
|
1320
|
+
const groups = new Map();
|
|
1321
|
+
for (const fn of allNodes.values()) {
|
|
1322
|
+
let key;
|
|
1323
|
+
let name;
|
|
1324
|
+
let isModule;
|
|
1325
|
+
if (fn.className) {
|
|
1326
|
+
key = `${fn.filePath}::${fn.className}`;
|
|
1327
|
+
name = fn.className;
|
|
1328
|
+
isModule = false;
|
|
1329
|
+
}
|
|
1330
|
+
else {
|
|
1331
|
+
// Synthetic module node — one per file
|
|
1332
|
+
key = fn.filePath;
|
|
1333
|
+
const base = fn.filePath.split('/').pop() ?? fn.filePath;
|
|
1334
|
+
name = '[' + base.replace(/\.[^.]+$/, '') + ']';
|
|
1335
|
+
isModule = true;
|
|
1336
|
+
}
|
|
1337
|
+
if (!groups.has(key)) {
|
|
1338
|
+
groups.set(key, { name, filePath: fn.filePath, language: fn.language, isModule, methods: [] });
|
|
1339
|
+
}
|
|
1340
|
+
groups.get(key).methods.push(fn);
|
|
1341
|
+
}
|
|
1342
|
+
// Build ClassNode[]
|
|
1343
|
+
const classMap = new Map();
|
|
1344
|
+
for (const [id, g] of groups) {
|
|
1345
|
+
const rel = relationships.get(id) ?? { parentClasses: [], interfaces: [] };
|
|
1346
|
+
const cls = {
|
|
1347
|
+
id,
|
|
1348
|
+
name: g.name,
|
|
1349
|
+
filePath: g.filePath,
|
|
1350
|
+
language: g.language,
|
|
1351
|
+
parentClasses: rel.parentClasses,
|
|
1352
|
+
interfaces: rel.interfaces,
|
|
1353
|
+
methodIds: g.methods.map(m => m.id),
|
|
1354
|
+
fanIn: g.methods.reduce((s, m) => s + m.fanIn, 0),
|
|
1355
|
+
fanOut: g.methods.reduce((s, m) => s + m.fanOut, 0),
|
|
1356
|
+
isModule: g.isModule,
|
|
1357
|
+
};
|
|
1358
|
+
classMap.set(id, cls);
|
|
1359
|
+
}
|
|
1360
|
+
// Build InheritanceEdge[] — only when both parent and child are in our graph
|
|
1361
|
+
// Parent lookup: match by class name across all ClassNodes (first match wins)
|
|
1362
|
+
const byName = new Map();
|
|
1363
|
+
for (const cls of classMap.values()) {
|
|
1364
|
+
if (!byName.has(cls.name))
|
|
1365
|
+
byName.set(cls.name, cls);
|
|
1366
|
+
}
|
|
1367
|
+
const inheritanceEdges = [];
|
|
1368
|
+
const seenEdges = new Set();
|
|
1369
|
+
for (const cls of classMap.values()) {
|
|
1370
|
+
for (const parentName of cls.parentClasses) {
|
|
1371
|
+
const parent = byName.get(parentName);
|
|
1372
|
+
if (!parent)
|
|
1373
|
+
continue;
|
|
1374
|
+
const edgeId = `${parent.id}->${cls.id}`;
|
|
1375
|
+
if (seenEdges.has(edgeId))
|
|
1376
|
+
continue;
|
|
1377
|
+
seenEdges.add(edgeId);
|
|
1378
|
+
// Go embedding vs OOP inheritance
|
|
1379
|
+
const kind = cls.language === 'Go' ? 'embeds' : 'extends';
|
|
1380
|
+
inheritanceEdges.push({ id: edgeId, parentId: parent.id, childId: cls.id, kind });
|
|
1381
|
+
}
|
|
1382
|
+
for (const ifaceName of cls.interfaces) {
|
|
1383
|
+
const parent = byName.get(ifaceName);
|
|
1384
|
+
if (!parent)
|
|
1385
|
+
continue;
|
|
1386
|
+
const edgeId = `${parent.id}->${cls.id}`;
|
|
1387
|
+
if (seenEdges.has(edgeId))
|
|
1388
|
+
continue;
|
|
1389
|
+
seenEdges.add(edgeId);
|
|
1390
|
+
inheritanceEdges.push({ id: edgeId, parentId: parent.id, childId: cls.id, kind: 'implements' });
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
return { classes: Array.from(classMap.values()), inheritanceEdges };
|
|
1394
|
+
}
|
|
1395
|
+
// ============================================================================
|
|
788
1396
|
// CALL GRAPH BUILDER
|
|
789
1397
|
// ============================================================================
|
|
790
1398
|
export class CallGraphBuilder {
|
|
791
1399
|
/**
|
|
792
1400
|
* Build a call graph from a list of source files.
|
|
793
1401
|
*
|
|
794
|
-
* @param files
|
|
795
|
-
* @param layers
|
|
796
|
-
*
|
|
1402
|
+
* @param files Source files with path, content, and language
|
|
1403
|
+
* @param layers Optional layer map { layerName: [path prefix, ...] }
|
|
1404
|
+
* @param importMap Optional per-file import map (from ImportResolverBridge)
|
|
797
1405
|
*/
|
|
798
|
-
async build(files, layers) {
|
|
1406
|
+
async build(files, layers, importMap) {
|
|
799
1407
|
const allNodes = new Map();
|
|
800
1408
|
const allRawEdges = [];
|
|
801
1409
|
// Pass 1: Extract nodes and raw edges from each file
|
|
@@ -823,6 +1431,9 @@ export class CallGraphBuilder {
|
|
|
823
1431
|
else if (file.language === 'C++') {
|
|
824
1432
|
result = await extractCppGraph(file.path, file.content);
|
|
825
1433
|
}
|
|
1434
|
+
else if (file.language === 'Swift') {
|
|
1435
|
+
result = await extractSwiftGraph(file.path, file.content);
|
|
1436
|
+
}
|
|
826
1437
|
else {
|
|
827
1438
|
continue;
|
|
828
1439
|
}
|
|
@@ -838,35 +1449,136 @@ export class CallGraphBuilder {
|
|
|
838
1449
|
}
|
|
839
1450
|
}
|
|
840
1451
|
}
|
|
841
|
-
// Pass 2: Resolve raw edges —
|
|
842
|
-
const
|
|
843
|
-
for (const node of allNodes.values())
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1452
|
+
// Pass 2: Resolve raw edges — multi-strategy resolution
|
|
1453
|
+
const trie = new FunctionRegistryTrie();
|
|
1454
|
+
for (const node of allNodes.values())
|
|
1455
|
+
trie.insert(node);
|
|
1456
|
+
// Build per-function-body content slices for type inference (keyed by functionId)
|
|
1457
|
+
const fileContents = new Map();
|
|
1458
|
+
for (const file of files)
|
|
1459
|
+
fileContents.set(file.path, file.content);
|
|
848
1460
|
const edges = [];
|
|
849
1461
|
for (const raw of allRawEdges) {
|
|
850
|
-
const
|
|
851
|
-
if (!
|
|
852
|
-
continue;
|
|
1462
|
+
const callerNode = allNodes.get(raw.callerId);
|
|
1463
|
+
if (!callerNode)
|
|
1464
|
+
continue;
|
|
853
1465
|
let calleeNode;
|
|
854
|
-
|
|
855
|
-
|
|
1466
|
+
let confidence = 'name_only';
|
|
1467
|
+
// Strategy 1 — self/cls intra-class (Python self.*, cls.* or same-class method)
|
|
1468
|
+
if (raw.calleeObject === 'self' || raw.calleeObject === 'cls') {
|
|
1469
|
+
if (callerNode.className) {
|
|
1470
|
+
const candidates = trie.findByQualifiedName(callerNode.className, raw.calleeName);
|
|
1471
|
+
if (candidates.length > 0) {
|
|
1472
|
+
calleeNode = candidates[0];
|
|
1473
|
+
confidence = 'self_cls';
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
856
1476
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1477
|
+
// Strategy 1b — Swift/C++ type-name resolution (capitalized receiver = type/class reference)
|
|
1478
|
+
// In Swift and C++, there are no intra-module imports, so cross-file calls appear as
|
|
1479
|
+
// TypeName.method() or TypeName::method(). A capitalized receiver with no same-file
|
|
1480
|
+
// class of that name is a reliable signal for a cross-file type reference.
|
|
1481
|
+
if (!calleeNode && raw.calleeObject && (callerNode.language === 'Swift' || callerNode.language === 'C++')) {
|
|
1482
|
+
const ch = raw.calleeObject.charCodeAt(0);
|
|
1483
|
+
const isCapitalized = ch >= 65 && ch <= 90; // A-Z
|
|
1484
|
+
if (isCapitalized) {
|
|
1485
|
+
const candidates = trie.findByQualifiedName(raw.calleeObject, raw.calleeName);
|
|
1486
|
+
if (candidates.length > 0) {
|
|
1487
|
+
calleeNode = candidates[0];
|
|
1488
|
+
confidence = 'type_name';
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
// Strategy 2 — type inference on receiver variable
|
|
1493
|
+
if (!calleeNode && raw.calleeObject) {
|
|
1494
|
+
const fileContent = fileContents.get(callerNode.filePath);
|
|
1495
|
+
if (fileContent) {
|
|
1496
|
+
const bodySlice = fileContent.slice(callerNode.startIndex, callerNode.endIndex);
|
|
1497
|
+
const inferredTypes = inferTypesFromSource(bodySlice, callerNode.language);
|
|
1498
|
+
const resolved = resolveViaTypeInference(raw.calleeObject, raw.calleeName, inferredTypes, trie);
|
|
1499
|
+
if (resolved) {
|
|
1500
|
+
calleeNode = resolved;
|
|
1501
|
+
confidence = 'type_inference';
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
// Strategy 3 — import resolution (TS/JS/Python/Go/Rust/Ruby/Java)
|
|
1506
|
+
if (!calleeNode && importMap) {
|
|
1507
|
+
const importedFile = importMap.get(callerNode.filePath)?.get(raw.calleeName)
|
|
1508
|
+
?? (raw.calleeObject ? importMap.get(callerNode.filePath)?.get(raw.calleeObject) : undefined);
|
|
1509
|
+
if (importedFile) {
|
|
1510
|
+
const candidates = trie.findBySimpleName(raw.calleeName).filter(n => n.filePath.startsWith(importedFile));
|
|
1511
|
+
if (candidates.length > 0) {
|
|
1512
|
+
calleeNode = candidates[0];
|
|
1513
|
+
confidence = 'import';
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
862
1516
|
}
|
|
1517
|
+
// Strategy 4 — same-file preference (only for calls without a typed receiver)
|
|
1518
|
+
// When a receiver is explicitly present but unresolvable (e.g. redis_client.get()),
|
|
1519
|
+
// skip name_only fallback to avoid false-positive edges.
|
|
1520
|
+
if (!calleeNode && !raw.calleeObject) {
|
|
1521
|
+
const candidates = trie.findBySimpleName(raw.calleeName);
|
|
1522
|
+
if (candidates.length === 0)
|
|
1523
|
+
continue; // external call, skip
|
|
1524
|
+
const sameFile = candidates.find(c => c.filePath === callerNode.filePath);
|
|
1525
|
+
if (sameFile) {
|
|
1526
|
+
calleeNode = sameFile;
|
|
1527
|
+
confidence = 'same_file';
|
|
1528
|
+
}
|
|
1529
|
+
else {
|
|
1530
|
+
calleeNode = candidates[0];
|
|
1531
|
+
confidence = 'name_only';
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
if (!calleeNode)
|
|
1535
|
+
continue;
|
|
863
1536
|
edges.push({
|
|
864
1537
|
callerId: raw.callerId,
|
|
865
1538
|
calleeId: calleeNode.id,
|
|
866
1539
|
calleeName: raw.calleeName,
|
|
867
1540
|
line: raw.line,
|
|
1541
|
+
confidence,
|
|
868
1542
|
});
|
|
869
1543
|
}
|
|
1544
|
+
// Pass 2b: HTTP cross-language edges (JS/TS caller → Python handler)
|
|
1545
|
+
try {
|
|
1546
|
+
const filePaths = files.map(f => f.path);
|
|
1547
|
+
const { edges: httpEdges } = await extractAllHttpEdges(filePaths);
|
|
1548
|
+
for (const he of httpEdges) {
|
|
1549
|
+
// Find callee: handler function by name in handlerFile
|
|
1550
|
+
const calleeNode = trie.findBySimpleName(he.route.handlerName)
|
|
1551
|
+
.find(n => n.filePath === he.handlerFile);
|
|
1552
|
+
if (!calleeNode)
|
|
1553
|
+
continue;
|
|
1554
|
+
// Find caller: any function in callerFile that encloses the HTTP call's line
|
|
1555
|
+
const callerContent = fileContents.get(he.callerFile);
|
|
1556
|
+
const callerNode = callerContent
|
|
1557
|
+
? (() => {
|
|
1558
|
+
let offset = 0;
|
|
1559
|
+
const lines = callerContent.split('\n');
|
|
1560
|
+
for (let i = 0; i < he.call.line - 1 && i < lines.length; i++) {
|
|
1561
|
+
offset += lines[i].length + 1;
|
|
1562
|
+
}
|
|
1563
|
+
const candidates = Array.from(allNodes.values())
|
|
1564
|
+
.filter(n => n.filePath === he.callerFile);
|
|
1565
|
+
return findEnclosingFunction(candidates, offset);
|
|
1566
|
+
})()
|
|
1567
|
+
: undefined;
|
|
1568
|
+
if (!callerNode)
|
|
1569
|
+
continue;
|
|
1570
|
+
edges.push({
|
|
1571
|
+
callerId: callerNode.id,
|
|
1572
|
+
calleeId: calleeNode.id,
|
|
1573
|
+
calleeName: he.route.handlerName,
|
|
1574
|
+
line: he.call.line,
|
|
1575
|
+
confidence: 'http_endpoint',
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
catch {
|
|
1580
|
+
// HTTP edge extraction is best-effort; don't fail the whole build
|
|
1581
|
+
}
|
|
870
1582
|
// Pass 3: Calculate fanIn / fanOut (count unique caller→callee pairs, not call sites)
|
|
871
1583
|
const seenPairs = new Set();
|
|
872
1584
|
for (const edge of edges) {
|
|
@@ -895,9 +1607,14 @@ export class CallGraphBuilder {
|
|
|
895
1607
|
: [];
|
|
896
1608
|
const totalFanIn = nodes.reduce((s, n) => s + n.fanIn, 0);
|
|
897
1609
|
const totalFanOut = nodes.reduce((s, n) => s + n.fanOut, 0);
|
|
1610
|
+
// Pass 5: Build class hierarchy (inheritance + grouping)
|
|
1611
|
+
const relationships = await extractClassRelationships(files);
|
|
1612
|
+
const { classes, inheritanceEdges } = buildClassNodes(allNodes, relationships);
|
|
898
1613
|
return {
|
|
899
1614
|
nodes: allNodes,
|
|
900
1615
|
edges,
|
|
1616
|
+
classes,
|
|
1617
|
+
inheritanceEdges,
|
|
901
1618
|
hubFunctions,
|
|
902
1619
|
entryPoints,
|
|
903
1620
|
layerViolations,
|
|
@@ -931,6 +1648,8 @@ export class CallGraphBuilder {
|
|
|
931
1648
|
continue;
|
|
932
1649
|
const callerIdx = layerOrder.indexOf(callerLayer);
|
|
933
1650
|
const calleeIdx = layerOrder.indexOf(calleeLayer);
|
|
1651
|
+
if (callerIdx === -1 || calleeIdx === -1)
|
|
1652
|
+
continue;
|
|
934
1653
|
if (callerIdx > calleeIdx) {
|
|
935
1654
|
// Lower layer calling upper layer — violation
|
|
936
1655
|
violations.push({
|
|
@@ -952,6 +1671,8 @@ export function serializeCallGraph(result) {
|
|
|
952
1671
|
return {
|
|
953
1672
|
nodes: Array.from(result.nodes.values()),
|
|
954
1673
|
edges: result.edges,
|
|
1674
|
+
classes: result.classes,
|
|
1675
|
+
inheritanceEdges: result.inheritanceEdges,
|
|
955
1676
|
hubFunctions: result.hubFunctions,
|
|
956
1677
|
entryPoints: result.entryPoints,
|
|
957
1678
|
layerViolations: result.layerViolations,
|