codemap-ai 3.4.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -1
- package/dist/chunk-6TAWVGMT.js +3539 -0
- package/dist/chunk-6TAWVGMT.js.map +1 -0
- package/dist/{chunk-VB74K47A.js → chunk-BRVRY5KT.js} +62 -1
- package/dist/chunk-BRVRY5KT.js.map +1 -0
- package/dist/{chunk-LXZ73T7X.js → chunk-EEMILSZ4.js} +58 -3
- package/dist/chunk-EEMILSZ4.js.map +1 -0
- package/dist/cli.js +38 -2927
- package/dist/cli.js.map +1 -1
- package/dist/{flow-server-4XSR5P4N.js → flow-server-U3IPHQ3N.js} +3 -3
- package/dist/{flow-server-4XSR5P4N.js.map → flow-server-U3IPHQ3N.js.map} +1 -1
- package/dist/index.js +628 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +2 -2
- package/dist/mcp-server.js.map +1 -1
- package/dist/visualize-LB2A4KEA.js +230 -0
- package/dist/visualize-LB2A4KEA.js.map +1 -0
- package/package.json +21 -3
- package/web-dist/assets/index-AyyZ2Kpr.js +96 -0
- package/web-dist/assets/index-CeA2-_I-.css +1 -0
- package/web-dist/index.html +14 -0
- package/dist/chunk-LXZ73T7X.js.map +0 -1
- package/dist/chunk-VB74K47A.js.map +0 -1
|
@@ -0,0 +1,3539 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
FlowStorage
|
|
5
|
+
} from "./chunk-BRVRY5KT.js";
|
|
6
|
+
|
|
7
|
+
// src/parsers/base.ts
|
|
8
|
+
var BaseParser = class {
|
|
9
|
+
nodeIdCounter = 0;
|
|
10
|
+
edgeIdCounter = 0;
|
|
11
|
+
generateNodeId(filePath, name, line) {
|
|
12
|
+
return `${filePath}:${name}:${line}`;
|
|
13
|
+
}
|
|
14
|
+
generateEdgeId() {
|
|
15
|
+
return `edge_${++this.edgeIdCounter}`;
|
|
16
|
+
}
|
|
17
|
+
createNode(type, name, filePath, startLine, endLine, metadata) {
|
|
18
|
+
return {
|
|
19
|
+
id: this.generateNodeId(filePath, name, startLine),
|
|
20
|
+
type,
|
|
21
|
+
name,
|
|
22
|
+
filePath,
|
|
23
|
+
startLine,
|
|
24
|
+
endLine,
|
|
25
|
+
language: this.language,
|
|
26
|
+
metadata
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
createEdge(type, sourceId, targetId, metadata) {
|
|
30
|
+
return {
|
|
31
|
+
id: this.generateEdgeId(),
|
|
32
|
+
type,
|
|
33
|
+
sourceId,
|
|
34
|
+
targetId,
|
|
35
|
+
metadata
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
createEmptyAnalysis(filePath) {
|
|
39
|
+
return {
|
|
40
|
+
filePath,
|
|
41
|
+
language: this.language,
|
|
42
|
+
nodes: [],
|
|
43
|
+
edges: [],
|
|
44
|
+
imports: [],
|
|
45
|
+
exports: []
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var ParserRegistry = class {
|
|
50
|
+
parsers = /* @__PURE__ */ new Map();
|
|
51
|
+
extensionMap = /* @__PURE__ */ new Map();
|
|
52
|
+
register(parser) {
|
|
53
|
+
this.parsers.set(parser.language, parser);
|
|
54
|
+
for (const ext of parser.extensions) {
|
|
55
|
+
this.extensionMap.set(ext, parser);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
getByLanguage(language) {
|
|
59
|
+
return this.parsers.get(language);
|
|
60
|
+
}
|
|
61
|
+
getByExtension(extension) {
|
|
62
|
+
const ext = extension.startsWith(".") ? extension : `.${extension}`;
|
|
63
|
+
return this.extensionMap.get(ext);
|
|
64
|
+
}
|
|
65
|
+
getForFile(filePath) {
|
|
66
|
+
const ext = filePath.substring(filePath.lastIndexOf("."));
|
|
67
|
+
return this.getByExtension(ext);
|
|
68
|
+
}
|
|
69
|
+
getSupportedExtensions() {
|
|
70
|
+
return Array.from(this.extensionMap.keys());
|
|
71
|
+
}
|
|
72
|
+
getSupportedLanguages() {
|
|
73
|
+
return Array.from(this.parsers.keys());
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
var parserRegistry = new ParserRegistry();
|
|
77
|
+
|
|
78
|
+
// src/parsers/typescript.ts
|
|
79
|
+
import Parser from "tree-sitter";
|
|
80
|
+
import TypeScript from "tree-sitter-typescript";
|
|
81
|
+
import JavaScript from "tree-sitter-javascript";
|
|
82
|
+
var TypeScriptParser = class extends BaseParser {
|
|
83
|
+
language = "typescript";
|
|
84
|
+
extensions = [".ts", ".tsx"];
|
|
85
|
+
parser;
|
|
86
|
+
treeSitterParser;
|
|
87
|
+
// Expose for Phase 3
|
|
88
|
+
constructor() {
|
|
89
|
+
super();
|
|
90
|
+
this.parser = new Parser();
|
|
91
|
+
this.parser.setLanguage(TypeScript.typescript);
|
|
92
|
+
this.treeSitterParser = this.parser;
|
|
93
|
+
}
|
|
94
|
+
parse(filePath, content) {
|
|
95
|
+
const analysis = this.createEmptyAnalysis(filePath);
|
|
96
|
+
const sourceCode = typeof content === "string" ? content : String(content);
|
|
97
|
+
if (sourceCode.length > 5e5) {
|
|
98
|
+
const lines2 = sourceCode.split("\n");
|
|
99
|
+
const fileNode2 = this.createNode(
|
|
100
|
+
"file",
|
|
101
|
+
filePath.split("/").pop() || filePath,
|
|
102
|
+
filePath,
|
|
103
|
+
1,
|
|
104
|
+
lines2.length
|
|
105
|
+
);
|
|
106
|
+
analysis.nodes.push(fileNode2);
|
|
107
|
+
return analysis;
|
|
108
|
+
}
|
|
109
|
+
if (filePath.endsWith(".tsx")) {
|
|
110
|
+
this.parser.setLanguage(TypeScript.tsx);
|
|
111
|
+
} else {
|
|
112
|
+
this.parser.setLanguage(TypeScript.typescript);
|
|
113
|
+
}
|
|
114
|
+
let tree;
|
|
115
|
+
try {
|
|
116
|
+
tree = this.parser.parse(sourceCode);
|
|
117
|
+
} catch {
|
|
118
|
+
const lines2 = sourceCode.split("\n");
|
|
119
|
+
const fileNode2 = this.createNode(
|
|
120
|
+
"file",
|
|
121
|
+
filePath.split("/").pop() || filePath,
|
|
122
|
+
filePath,
|
|
123
|
+
1,
|
|
124
|
+
lines2.length
|
|
125
|
+
);
|
|
126
|
+
analysis.nodes.push(fileNode2);
|
|
127
|
+
return analysis;
|
|
128
|
+
}
|
|
129
|
+
const lines = sourceCode.split("\n");
|
|
130
|
+
const fileNode = this.createNode(
|
|
131
|
+
"file",
|
|
132
|
+
filePath.split("/").pop() || filePath,
|
|
133
|
+
filePath,
|
|
134
|
+
1,
|
|
135
|
+
lines.length
|
|
136
|
+
);
|
|
137
|
+
analysis.nodes.push(fileNode);
|
|
138
|
+
this.walkTree(tree.rootNode, filePath, analysis, fileNode.id);
|
|
139
|
+
return analysis;
|
|
140
|
+
}
|
|
141
|
+
walkTree(node, filePath, analysis, parentId) {
|
|
142
|
+
switch (node.type) {
|
|
143
|
+
case "import_statement":
|
|
144
|
+
this.handleImport(node, analysis);
|
|
145
|
+
break;
|
|
146
|
+
case "export_statement":
|
|
147
|
+
this.handleExport(node, filePath, analysis, parentId);
|
|
148
|
+
break;
|
|
149
|
+
case "function_declaration":
|
|
150
|
+
case "arrow_function":
|
|
151
|
+
case "function_expression":
|
|
152
|
+
this.handleFunction(node, filePath, analysis, parentId);
|
|
153
|
+
break;
|
|
154
|
+
case "class_declaration":
|
|
155
|
+
this.handleClass(node, filePath, analysis, parentId);
|
|
156
|
+
break;
|
|
157
|
+
case "method_definition":
|
|
158
|
+
this.handleMethod(node, filePath, analysis, parentId);
|
|
159
|
+
break;
|
|
160
|
+
case "call_expression":
|
|
161
|
+
this.handleCall(node, filePath, analysis, parentId);
|
|
162
|
+
break;
|
|
163
|
+
case "variable_declaration":
|
|
164
|
+
this.handleVariable(node, filePath, analysis, parentId);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
for (const child of node.children) {
|
|
168
|
+
this.walkTree(child, filePath, analysis, parentId);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
handleImport(node, analysis) {
|
|
172
|
+
const sourceNode = node.childForFieldName("source");
|
|
173
|
+
if (!sourceNode) return;
|
|
174
|
+
const source = sourceNode.text.replace(/['"]/g, "");
|
|
175
|
+
const specifiers = [];
|
|
176
|
+
let isDefault = false;
|
|
177
|
+
let isNamespace = false;
|
|
178
|
+
for (const child of node.children) {
|
|
179
|
+
if (child.type === "import_clause") {
|
|
180
|
+
for (const clauseChild of child.children) {
|
|
181
|
+
if (clauseChild.type === "identifier") {
|
|
182
|
+
specifiers.push(clauseChild.text);
|
|
183
|
+
isDefault = true;
|
|
184
|
+
} else if (clauseChild.type === "namespace_import") {
|
|
185
|
+
isNamespace = true;
|
|
186
|
+
const nameNode = clauseChild.childForFieldName("name");
|
|
187
|
+
if (nameNode) specifiers.push(nameNode.text);
|
|
188
|
+
} else if (clauseChild.type === "named_imports") {
|
|
189
|
+
for (const importSpec of clauseChild.children) {
|
|
190
|
+
if (importSpec.type === "import_specifier") {
|
|
191
|
+
const name = importSpec.childForFieldName("name");
|
|
192
|
+
if (name) specifiers.push(name.text);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
analysis.imports.push({
|
|
200
|
+
source,
|
|
201
|
+
specifiers,
|
|
202
|
+
isDefault,
|
|
203
|
+
isNamespace,
|
|
204
|
+
line: node.startPosition.row + 1
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
handleExport(node, filePath, analysis, parentId) {
|
|
208
|
+
const isDefault = node.children.some((c) => c.type === "default");
|
|
209
|
+
for (const child of node.children) {
|
|
210
|
+
if (child.type === "function_declaration") {
|
|
211
|
+
const nameNode = child.childForFieldName("name");
|
|
212
|
+
if (nameNode) {
|
|
213
|
+
analysis.exports.push({
|
|
214
|
+
name: nameNode.text,
|
|
215
|
+
isDefault,
|
|
216
|
+
line: node.startPosition.row + 1
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
this.handleFunction(child, filePath, analysis, parentId);
|
|
220
|
+
} else if (child.type === "class_declaration") {
|
|
221
|
+
const nameNode = child.childForFieldName("name");
|
|
222
|
+
if (nameNode) {
|
|
223
|
+
analysis.exports.push({
|
|
224
|
+
name: nameNode.text,
|
|
225
|
+
isDefault,
|
|
226
|
+
line: node.startPosition.row + 1
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
this.handleClass(child, filePath, analysis, parentId);
|
|
230
|
+
} else if (child.type === "identifier") {
|
|
231
|
+
analysis.exports.push({
|
|
232
|
+
name: child.text,
|
|
233
|
+
isDefault,
|
|
234
|
+
line: node.startPosition.row + 1
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
handleFunction(node, filePath, analysis, parentId) {
|
|
240
|
+
let name = "anonymous";
|
|
241
|
+
const nameNode = node.childForFieldName("name");
|
|
242
|
+
if (nameNode) {
|
|
243
|
+
name = nameNode.text;
|
|
244
|
+
}
|
|
245
|
+
const funcNode = this.createNode(
|
|
246
|
+
"function",
|
|
247
|
+
name,
|
|
248
|
+
filePath,
|
|
249
|
+
node.startPosition.row + 1,
|
|
250
|
+
node.endPosition.row + 1,
|
|
251
|
+
{
|
|
252
|
+
async: node.children.some((c) => c.type === "async"),
|
|
253
|
+
generator: node.children.some((c) => c.text === "*")
|
|
254
|
+
}
|
|
255
|
+
);
|
|
256
|
+
analysis.nodes.push(funcNode);
|
|
257
|
+
analysis.edges.push(
|
|
258
|
+
this.createEdge("contains", parentId, funcNode.id)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
handleClass(node, filePath, analysis, parentId) {
|
|
262
|
+
const nameNode = node.childForFieldName("name");
|
|
263
|
+
if (!nameNode) return;
|
|
264
|
+
const extends_ = [];
|
|
265
|
+
for (const child of node.children) {
|
|
266
|
+
if (child.type === "class_heritage") {
|
|
267
|
+
const extendsClause = child.children.find((c) => c.type === "extends_clause");
|
|
268
|
+
if (extendsClause) {
|
|
269
|
+
const baseClass = extendsClause.children.find((c) => c.type === "identifier");
|
|
270
|
+
if (baseClass) {
|
|
271
|
+
extends_.push(baseClass.text);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const classNode = this.createNode(
|
|
277
|
+
"class",
|
|
278
|
+
nameNode.text,
|
|
279
|
+
filePath,
|
|
280
|
+
node.startPosition.row + 1,
|
|
281
|
+
node.endPosition.row + 1,
|
|
282
|
+
{
|
|
283
|
+
extends: extends_
|
|
284
|
+
}
|
|
285
|
+
);
|
|
286
|
+
analysis.nodes.push(classNode);
|
|
287
|
+
analysis.edges.push(
|
|
288
|
+
this.createEdge("contains", parentId, classNode.id)
|
|
289
|
+
);
|
|
290
|
+
for (const baseClass of extends_) {
|
|
291
|
+
analysis.edges.push(
|
|
292
|
+
this.createEdge("extends", classNode.id, `ref:${baseClass}`, {
|
|
293
|
+
unresolvedName: baseClass
|
|
294
|
+
})
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
const body = node.childForFieldName("body");
|
|
298
|
+
if (body) {
|
|
299
|
+
for (const member of body.children) {
|
|
300
|
+
if (member.type === "method_definition") {
|
|
301
|
+
this.handleMethod(member, filePath, analysis, classNode.id);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
handleMethod(node, filePath, analysis, parentId) {
|
|
307
|
+
const nameNode = node.childForFieldName("name");
|
|
308
|
+
if (!nameNode) return;
|
|
309
|
+
const parentNode = analysis.nodes.find((n) => n.id === parentId);
|
|
310
|
+
const parentClass = parentNode?.type === "class" ? parentNode.name : void 0;
|
|
311
|
+
const methodNode = this.createNode(
|
|
312
|
+
"method",
|
|
313
|
+
nameNode.text,
|
|
314
|
+
filePath,
|
|
315
|
+
node.startPosition.row + 1,
|
|
316
|
+
node.endPosition.row + 1,
|
|
317
|
+
{
|
|
318
|
+
static: node.children.some((c) => c.type === "static"),
|
|
319
|
+
async: node.children.some((c) => c.type === "async"),
|
|
320
|
+
parentClass
|
|
321
|
+
}
|
|
322
|
+
);
|
|
323
|
+
analysis.nodes.push(methodNode);
|
|
324
|
+
analysis.edges.push(
|
|
325
|
+
this.createEdge("contains", parentId, methodNode.id)
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
handleCall(node, filePath, analysis, parentId) {
|
|
329
|
+
const funcNode = node.childForFieldName("function");
|
|
330
|
+
if (!funcNode) return;
|
|
331
|
+
let calledName = "";
|
|
332
|
+
if (funcNode.type === "identifier") {
|
|
333
|
+
calledName = funcNode.text;
|
|
334
|
+
} else if (funcNode.type === "member_expression") {
|
|
335
|
+
calledName = funcNode.text;
|
|
336
|
+
}
|
|
337
|
+
if (calledName) {
|
|
338
|
+
analysis.edges.push(
|
|
339
|
+
this.createEdge("calls", parentId, `ref:${calledName}`, {
|
|
340
|
+
unresolvedName: calledName,
|
|
341
|
+
line: node.startPosition.row + 1
|
|
342
|
+
})
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
handleVariable(node, filePath, analysis, parentId) {
|
|
347
|
+
for (const child of node.children) {
|
|
348
|
+
if (child.type === "variable_declarator") {
|
|
349
|
+
const nameNode = child.childForFieldName("name");
|
|
350
|
+
const valueNode = child.childForFieldName("value");
|
|
351
|
+
if (nameNode && nameNode.type === "identifier") {
|
|
352
|
+
if (valueNode && (valueNode.type === "arrow_function" || valueNode.type === "function_expression")) {
|
|
353
|
+
const funcNode = this.createNode(
|
|
354
|
+
"function",
|
|
355
|
+
nameNode.text,
|
|
356
|
+
filePath,
|
|
357
|
+
node.startPosition.row + 1,
|
|
358
|
+
node.endPosition.row + 1,
|
|
359
|
+
{
|
|
360
|
+
kind: node.children[0]?.text || "const"
|
|
361
|
+
}
|
|
362
|
+
);
|
|
363
|
+
analysis.nodes.push(funcNode);
|
|
364
|
+
analysis.edges.push(
|
|
365
|
+
this.createEdge("contains", parentId, funcNode.id)
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
var JavaScriptParser = class extends TypeScriptParser {
|
|
374
|
+
language = "javascript";
|
|
375
|
+
extensions = [".js", ".jsx", ".mjs", ".cjs"];
|
|
376
|
+
constructor() {
|
|
377
|
+
super();
|
|
378
|
+
this.parser = new Parser();
|
|
379
|
+
this.parser.setLanguage(JavaScript);
|
|
380
|
+
this.treeSitterParser = this.parser;
|
|
381
|
+
}
|
|
382
|
+
parse(filePath, content) {
|
|
383
|
+
const sourceCode = typeof content === "string" ? content : String(content);
|
|
384
|
+
if (filePath.endsWith(".jsx")) {
|
|
385
|
+
this.parser.setLanguage(TypeScript.tsx);
|
|
386
|
+
} else {
|
|
387
|
+
this.parser.setLanguage(JavaScript);
|
|
388
|
+
}
|
|
389
|
+
const analysis = super.parse(filePath, sourceCode);
|
|
390
|
+
analysis.language = "javascript";
|
|
391
|
+
for (const node of analysis.nodes) {
|
|
392
|
+
node.language = "javascript";
|
|
393
|
+
}
|
|
394
|
+
return analysis;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// src/parsers/python.ts
|
|
399
|
+
import Parser2 from "tree-sitter";
|
|
400
|
+
import Python from "tree-sitter-python";
|
|
401
|
+
var PythonParser = class extends BaseParser {
|
|
402
|
+
language = "python";
|
|
403
|
+
extensions = [".py"];
|
|
404
|
+
parser;
|
|
405
|
+
treeSitterParser;
|
|
406
|
+
// Expose for Phase 3
|
|
407
|
+
constructor() {
|
|
408
|
+
super();
|
|
409
|
+
this.parser = new Parser2();
|
|
410
|
+
this.parser.setLanguage(Python);
|
|
411
|
+
this.treeSitterParser = this.parser;
|
|
412
|
+
}
|
|
413
|
+
parse(filePath, content) {
|
|
414
|
+
const analysis = this.createEmptyAnalysis(filePath);
|
|
415
|
+
const sourceCode = typeof content === "string" ? content : String(content);
|
|
416
|
+
if (sourceCode.length > 5e5) {
|
|
417
|
+
const lines2 = sourceCode.split("\n");
|
|
418
|
+
const fileNode2 = this.createNode(
|
|
419
|
+
"file",
|
|
420
|
+
filePath.split("/").pop() || filePath,
|
|
421
|
+
filePath,
|
|
422
|
+
1,
|
|
423
|
+
lines2.length
|
|
424
|
+
);
|
|
425
|
+
analysis.nodes.push(fileNode2);
|
|
426
|
+
return analysis;
|
|
427
|
+
}
|
|
428
|
+
let tree;
|
|
429
|
+
try {
|
|
430
|
+
tree = this.parser.parse((index, position) => {
|
|
431
|
+
if (index >= sourceCode.length) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const endIndex = Math.min(index + 1024, sourceCode.length);
|
|
435
|
+
return sourceCode.substring(index, endIndex);
|
|
436
|
+
});
|
|
437
|
+
} catch (error) {
|
|
438
|
+
const lines2 = sourceCode.split("\n");
|
|
439
|
+
const fileNode2 = this.createNode(
|
|
440
|
+
"file",
|
|
441
|
+
filePath.split("/").pop() || filePath,
|
|
442
|
+
filePath,
|
|
443
|
+
1,
|
|
444
|
+
lines2.length
|
|
445
|
+
);
|
|
446
|
+
analysis.nodes.push(fileNode2);
|
|
447
|
+
return analysis;
|
|
448
|
+
}
|
|
449
|
+
const lines = sourceCode.split("\n");
|
|
450
|
+
const fileNode = this.createNode(
|
|
451
|
+
"file",
|
|
452
|
+
filePath.split("/").pop() || filePath,
|
|
453
|
+
filePath,
|
|
454
|
+
1,
|
|
455
|
+
lines.length
|
|
456
|
+
);
|
|
457
|
+
analysis.nodes.push(fileNode);
|
|
458
|
+
this.walkTree(tree.rootNode, filePath, analysis, fileNode.id);
|
|
459
|
+
return analysis;
|
|
460
|
+
}
|
|
461
|
+
walkTree(node, filePath, analysis, parentId) {
|
|
462
|
+
switch (node.type) {
|
|
463
|
+
case "import_statement":
|
|
464
|
+
this.handleImport(node, analysis);
|
|
465
|
+
break;
|
|
466
|
+
case "import_from_statement":
|
|
467
|
+
this.handleFromImport(node, analysis);
|
|
468
|
+
break;
|
|
469
|
+
case "function_definition":
|
|
470
|
+
this.handleFunction(node, filePath, analysis, parentId);
|
|
471
|
+
return;
|
|
472
|
+
// Don't recurse into function body for top-level analysis
|
|
473
|
+
case "class_definition":
|
|
474
|
+
this.handleClass(node, filePath, analysis, parentId);
|
|
475
|
+
return;
|
|
476
|
+
// Don't recurse, class handler will process methods
|
|
477
|
+
case "call":
|
|
478
|
+
this.handleCall(node, filePath, analysis, parentId);
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
for (const child of node.children) {
|
|
482
|
+
this.walkTree(child, filePath, analysis, parentId);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
handleImport(node, analysis) {
|
|
486
|
+
for (const child of node.children) {
|
|
487
|
+
if (child.type === "dotted_name") {
|
|
488
|
+
analysis.imports.push({
|
|
489
|
+
source: child.text,
|
|
490
|
+
specifiers: [child.text.split(".").pop() || child.text],
|
|
491
|
+
isDefault: false,
|
|
492
|
+
isNamespace: true,
|
|
493
|
+
line: node.startPosition.row + 1
|
|
494
|
+
});
|
|
495
|
+
} else if (child.type === "aliased_import") {
|
|
496
|
+
const nameNode = child.childForFieldName("name");
|
|
497
|
+
const aliasNode = child.childForFieldName("alias");
|
|
498
|
+
if (nameNode) {
|
|
499
|
+
analysis.imports.push({
|
|
500
|
+
source: nameNode.text,
|
|
501
|
+
specifiers: [aliasNode?.text || nameNode.text],
|
|
502
|
+
isDefault: false,
|
|
503
|
+
isNamespace: true,
|
|
504
|
+
line: node.startPosition.row + 1
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
handleFromImport(node, analysis) {
|
|
511
|
+
const moduleNode = node.childForFieldName("module_name");
|
|
512
|
+
if (!moduleNode) return;
|
|
513
|
+
const source = moduleNode.text;
|
|
514
|
+
const specifiers = [];
|
|
515
|
+
for (const child of node.children) {
|
|
516
|
+
if (child.type === "import_prefix") {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (child.type === "dotted_name" && child !== moduleNode) {
|
|
520
|
+
specifiers.push(child.text);
|
|
521
|
+
} else if (child.type === "aliased_import") {
|
|
522
|
+
const nameNode = child.childForFieldName("name");
|
|
523
|
+
if (nameNode) {
|
|
524
|
+
specifiers.push(nameNode.text);
|
|
525
|
+
}
|
|
526
|
+
} else if (child.type === "wildcard_import") {
|
|
527
|
+
specifiers.push("*");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (specifiers.length === 0) {
|
|
531
|
+
for (const child of node.children) {
|
|
532
|
+
if (child.type === "import_list" || child.type === "import_from_specifier") {
|
|
533
|
+
for (const spec of child.children) {
|
|
534
|
+
if (spec.type === "dotted_name" || spec.type === "identifier") {
|
|
535
|
+
specifiers.push(spec.text);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
analysis.imports.push({
|
|
542
|
+
source,
|
|
543
|
+
specifiers,
|
|
544
|
+
isDefault: false,
|
|
545
|
+
isNamespace: specifiers.includes("*"),
|
|
546
|
+
line: node.startPosition.row + 1
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
handleFunction(node, filePath, analysis, parentId) {
|
|
550
|
+
const nameNode = node.childForFieldName("name");
|
|
551
|
+
if (!nameNode) return;
|
|
552
|
+
const name = nameNode.text;
|
|
553
|
+
const decorators = [];
|
|
554
|
+
const decoratorDetails = [];
|
|
555
|
+
if (node.parent?.type === "decorated_definition") {
|
|
556
|
+
for (const sibling of node.parent.children) {
|
|
557
|
+
if (sibling.type === "decorator") {
|
|
558
|
+
const decoratorCall = sibling.children.find((c) => c.type === "call");
|
|
559
|
+
const decoratorAttr = sibling.children.find((c) => c.type === "attribute" || c.type === "identifier");
|
|
560
|
+
if (decoratorCall) {
|
|
561
|
+
const funcName = decoratorCall.children.find((c) => c.type === "attribute" || c.type === "identifier");
|
|
562
|
+
if (funcName) {
|
|
563
|
+
const fullDecorator = funcName.text;
|
|
564
|
+
decorators.push(fullDecorator);
|
|
565
|
+
const args = [];
|
|
566
|
+
const kwargs = {};
|
|
567
|
+
const argList = decoratorCall.children.find((c) => c.type === "argument_list");
|
|
568
|
+
if (argList) {
|
|
569
|
+
for (const arg of argList.children) {
|
|
570
|
+
if (arg.type === "string") {
|
|
571
|
+
const stringContent = arg.children.find((c) => c.type === "string_content");
|
|
572
|
+
const argText = stringContent ? stringContent.text : arg.text.replace(/^["']|["']$/g, "");
|
|
573
|
+
args.push(argText);
|
|
574
|
+
} else if (arg.type === "keyword_argument") {
|
|
575
|
+
const keyNode = arg.childForFieldName("name");
|
|
576
|
+
const valueNode = arg.childForFieldName("value");
|
|
577
|
+
if (keyNode && valueNode) {
|
|
578
|
+
const key = keyNode.text;
|
|
579
|
+
let value = valueNode.text;
|
|
580
|
+
if (valueNode.type === "list") {
|
|
581
|
+
const items = [];
|
|
582
|
+
for (const child of valueNode.children) {
|
|
583
|
+
if (child.type === "string") {
|
|
584
|
+
const stringContent = child.children.find((c) => c.type === "string_content");
|
|
585
|
+
items.push(stringContent ? stringContent.text : child.text.replace(/^["']|["']$/g, ""));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
value = items.length > 0 ? items.join(",") : value;
|
|
589
|
+
}
|
|
590
|
+
kwargs[key] = value;
|
|
591
|
+
}
|
|
592
|
+
} else if (arg.type === "identifier") {
|
|
593
|
+
args.push(arg.text);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
decoratorDetails.push({ name: fullDecorator, args, kwargs });
|
|
598
|
+
}
|
|
599
|
+
} else if (decoratorAttr) {
|
|
600
|
+
const fullDecorator = decoratorAttr.text;
|
|
601
|
+
decorators.push(fullDecorator);
|
|
602
|
+
decoratorDetails.push({ name: fullDecorator, args: [] });
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const isAsync = node.children.some((c) => c.type === "async");
|
|
608
|
+
let returnType;
|
|
609
|
+
const returnTypeNode = node.childForFieldName("return_type");
|
|
610
|
+
if (returnTypeNode) {
|
|
611
|
+
returnType = returnTypeNode.text;
|
|
612
|
+
returnType = returnType.replace(/["']/g, "").split("[")[0].trim();
|
|
613
|
+
}
|
|
614
|
+
const funcNode = this.createNode(
|
|
615
|
+
"function",
|
|
616
|
+
name,
|
|
617
|
+
filePath,
|
|
618
|
+
node.startPosition.row + 1,
|
|
619
|
+
node.endPosition.row + 1,
|
|
620
|
+
{
|
|
621
|
+
async: isAsync,
|
|
622
|
+
decorators,
|
|
623
|
+
decoratorDetails: JSON.stringify(decoratorDetails),
|
|
624
|
+
isPrivate: name.startsWith("_"),
|
|
625
|
+
isDunder: name.startsWith("__") && name.endsWith("__"),
|
|
626
|
+
returnType
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
analysis.nodes.push(funcNode);
|
|
630
|
+
analysis.edges.push(this.createEdge("contains", parentId, funcNode.id));
|
|
631
|
+
this.detectFastAPIRoute(decoratorDetails, funcNode, analysis);
|
|
632
|
+
const parameters = node.childForFieldName("parameters");
|
|
633
|
+
if (parameters) {
|
|
634
|
+
this.detectDependsPattern(parameters, funcNode, analysis);
|
|
635
|
+
this.extractParameterTypes(parameters, funcNode.id, filePath, analysis);
|
|
636
|
+
}
|
|
637
|
+
const body = node.childForFieldName("body");
|
|
638
|
+
if (body) {
|
|
639
|
+
this.parseBodyForCalls(body, filePath, analysis, funcNode.id);
|
|
640
|
+
}
|
|
641
|
+
if (!name.startsWith("_")) {
|
|
642
|
+
analysis.exports.push({
|
|
643
|
+
name,
|
|
644
|
+
isDefault: false,
|
|
645
|
+
line: node.startPosition.row + 1
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Detect FastAPI route decorators like @router.post("/chat")
|
|
651
|
+
*/
|
|
652
|
+
detectFastAPIRoute(decoratorDetails, funcNode, analysis) {
|
|
653
|
+
for (const dec of decoratorDetails) {
|
|
654
|
+
const routeMatch = dec.name.match(/^(router|app)\.(get|post|put|patch|delete|options|head)$/);
|
|
655
|
+
if (routeMatch && dec.args.length > 0) {
|
|
656
|
+
const method = routeMatch[2].toUpperCase();
|
|
657
|
+
const path = dec.args[0];
|
|
658
|
+
if (!funcNode.metadata) funcNode.metadata = {};
|
|
659
|
+
funcNode.metadata.httpEndpoint = {
|
|
660
|
+
method,
|
|
661
|
+
path
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Detect Depends() pattern in function parameters
|
|
668
|
+
* Example: user: User = Depends(authenticate_user)
|
|
669
|
+
*/
|
|
670
|
+
detectDependsPattern(parameters, funcNode, analysis) {
|
|
671
|
+
for (const param of parameters.children) {
|
|
672
|
+
if (param.type === "typed_parameter" || param.type === "default_parameter") {
|
|
673
|
+
const defaultValue = param.children.find((c) => c.type === "call");
|
|
674
|
+
if (defaultValue) {
|
|
675
|
+
const funcCall = defaultValue.childForFieldName("function");
|
|
676
|
+
if (funcCall && funcCall.text === "Depends") {
|
|
677
|
+
const argList = defaultValue.childForFieldName("arguments");
|
|
678
|
+
if (argList) {
|
|
679
|
+
for (const arg of argList.children) {
|
|
680
|
+
if (arg.type === "identifier") {
|
|
681
|
+
const dependencyName = arg.text;
|
|
682
|
+
const paramName = param.children.find(
|
|
683
|
+
(c) => c.type === "identifier"
|
|
684
|
+
)?.text;
|
|
685
|
+
if (!funcNode.metadata) funcNode.metadata = {};
|
|
686
|
+
if (!funcNode.metadata.depends) funcNode.metadata.depends = [];
|
|
687
|
+
funcNode.metadata.depends.push({
|
|
688
|
+
parameterName: paramName,
|
|
689
|
+
dependencyName,
|
|
690
|
+
line: param.startPosition.row + 1
|
|
691
|
+
});
|
|
692
|
+
analysis.edges.push(
|
|
693
|
+
this.createEdge("depends", funcNode.id, `ref:${dependencyName}`, {
|
|
694
|
+
unresolvedName: dependencyName,
|
|
695
|
+
parameterName: paramName,
|
|
696
|
+
line: param.startPosition.row + 1
|
|
697
|
+
})
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Extract parameter type hints and register as variable types
|
|
709
|
+
* Example: def process(user: User, config: Config)
|
|
710
|
+
*/
|
|
711
|
+
extractParameterTypes(parameters, scopeId, filePath, analysis) {
|
|
712
|
+
for (const param of parameters.children) {
|
|
713
|
+
if (param.type === "typed_parameter") {
|
|
714
|
+
const nameNode = param.children.find((c) => c.type === "identifier");
|
|
715
|
+
if (!nameNode) continue;
|
|
716
|
+
const paramName = nameNode.text;
|
|
717
|
+
if (paramName === "self" || paramName === "cls") continue;
|
|
718
|
+
const typeNode = param.children.find((c) => c.type === "type");
|
|
719
|
+
if (!typeNode) continue;
|
|
720
|
+
let typeName = typeNode.text;
|
|
721
|
+
typeName = typeName.replace(/["']/g, "").trim();
|
|
722
|
+
const genericMatch = typeName.match(/(?:Optional|List|Dict|Set)\[([^\]]+)\]/);
|
|
723
|
+
if (genericMatch) {
|
|
724
|
+
typeName = genericMatch[1].trim();
|
|
725
|
+
}
|
|
726
|
+
if (!analysis.variableTypes) {
|
|
727
|
+
analysis.variableTypes = [];
|
|
728
|
+
}
|
|
729
|
+
analysis.variableTypes.push({
|
|
730
|
+
variableName: paramName,
|
|
731
|
+
typeName,
|
|
732
|
+
scopeId,
|
|
733
|
+
line: param.startPosition.row + 1
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
handleClass(node, filePath, analysis, parentId) {
|
|
739
|
+
const nameNode = node.childForFieldName("name");
|
|
740
|
+
if (!nameNode) return;
|
|
741
|
+
const name = nameNode.text;
|
|
742
|
+
const bases = [];
|
|
743
|
+
const argumentList = node.children.find((c) => c.type === "argument_list");
|
|
744
|
+
if (argumentList) {
|
|
745
|
+
for (const arg of argumentList.children) {
|
|
746
|
+
if (arg.type === "identifier" || arg.type === "attribute") {
|
|
747
|
+
bases.push(arg.text);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const classNode = this.createNode(
|
|
752
|
+
"class",
|
|
753
|
+
name,
|
|
754
|
+
filePath,
|
|
755
|
+
node.startPosition.row + 1,
|
|
756
|
+
node.endPosition.row + 1,
|
|
757
|
+
{
|
|
758
|
+
bases,
|
|
759
|
+
extends: bases,
|
|
760
|
+
// Add extends for consistency with TypeScript
|
|
761
|
+
isPrivate: name.startsWith("_")
|
|
762
|
+
}
|
|
763
|
+
);
|
|
764
|
+
analysis.nodes.push(classNode);
|
|
765
|
+
analysis.edges.push(this.createEdge("contains", parentId, classNode.id));
|
|
766
|
+
for (const base of bases) {
|
|
767
|
+
analysis.edges.push(
|
|
768
|
+
this.createEdge("extends", classNode.id, `ref:${base}`, {
|
|
769
|
+
unresolvedName: base
|
|
770
|
+
})
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
const body = node.childForFieldName("body");
|
|
774
|
+
if (body) {
|
|
775
|
+
for (const child of body.children) {
|
|
776
|
+
if (child.type === "function_definition") {
|
|
777
|
+
this.handleMethod(child, filePath, analysis, classNode.id);
|
|
778
|
+
} else if (child.type === "decorated_definition") {
|
|
779
|
+
const funcNode = child.children.find((c) => c.type === "function_definition");
|
|
780
|
+
if (funcNode) {
|
|
781
|
+
this.handleMethod(funcNode, filePath, analysis, classNode.id);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (!name.startsWith("_")) {
|
|
787
|
+
analysis.exports.push({
|
|
788
|
+
name,
|
|
789
|
+
isDefault: false,
|
|
790
|
+
line: node.startPosition.row + 1
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
handleMethod(node, filePath, analysis, classId) {
|
|
795
|
+
const nameNode = node.childForFieldName("name");
|
|
796
|
+
if (!nameNode) return;
|
|
797
|
+
const name = nameNode.text;
|
|
798
|
+
const parentNode = analysis.nodes.find((n) => n.id === classId);
|
|
799
|
+
const parentClass = parentNode?.type === "class" ? parentNode.name : void 0;
|
|
800
|
+
const decorators = [];
|
|
801
|
+
if (node.parent?.type === "decorated_definition") {
|
|
802
|
+
for (const sibling of node.parent.children) {
|
|
803
|
+
if (sibling.type === "decorator") {
|
|
804
|
+
const decoratorAttr = sibling.children.find((c) => c.type === "attribute" || c.type === "identifier");
|
|
805
|
+
if (decoratorAttr) {
|
|
806
|
+
decorators.push(decoratorAttr.text);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const isStatic = decorators.includes("staticmethod");
|
|
812
|
+
const isClassMethod = decorators.includes("classmethod");
|
|
813
|
+
const isProperty = decorators.includes("property");
|
|
814
|
+
const isAsync = node.children.some((c) => c.type === "async");
|
|
815
|
+
const methodNode = this.createNode(
|
|
816
|
+
"method",
|
|
817
|
+
name,
|
|
818
|
+
filePath,
|
|
819
|
+
node.startPosition.row + 1,
|
|
820
|
+
node.endPosition.row + 1,
|
|
821
|
+
{
|
|
822
|
+
async: isAsync,
|
|
823
|
+
decorators,
|
|
824
|
+
static: isStatic,
|
|
825
|
+
classmethod: isClassMethod,
|
|
826
|
+
property: isProperty,
|
|
827
|
+
isPrivate: name.startsWith("_"),
|
|
828
|
+
isDunder: name.startsWith("__") && name.endsWith("__"),
|
|
829
|
+
parentClass
|
|
830
|
+
}
|
|
831
|
+
);
|
|
832
|
+
analysis.nodes.push(methodNode);
|
|
833
|
+
analysis.edges.push(this.createEdge("contains", classId, methodNode.id));
|
|
834
|
+
const body = node.childForFieldName("body");
|
|
835
|
+
if (body) {
|
|
836
|
+
this.parseBodyForCalls(body, filePath, analysis, methodNode.id);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
handleCall(node, filePath, analysis, parentId) {
|
|
840
|
+
const funcNode = node.childForFieldName("function");
|
|
841
|
+
if (!funcNode) return;
|
|
842
|
+
let calledName = "";
|
|
843
|
+
if (funcNode.type === "identifier") {
|
|
844
|
+
calledName = funcNode.text;
|
|
845
|
+
} else if (funcNode.type === "attribute") {
|
|
846
|
+
calledName = funcNode.text;
|
|
847
|
+
}
|
|
848
|
+
if (calledName) {
|
|
849
|
+
analysis.edges.push(
|
|
850
|
+
this.createEdge("calls", parentId, `ref:${calledName}`, {
|
|
851
|
+
unresolvedName: calledName,
|
|
852
|
+
line: node.startPosition.row + 1
|
|
853
|
+
})
|
|
854
|
+
);
|
|
855
|
+
this.detectDatabaseOperation(calledName, node, parentId, analysis);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Detect database operations (MongoDB, SQL, etc.)
|
|
860
|
+
* Examples: collection.find_one(), db.users.insert_one(), collection.update_many()
|
|
861
|
+
*/
|
|
862
|
+
detectDatabaseOperation(calledName, node, parentId, analysis) {
|
|
863
|
+
const mongoOps = [
|
|
864
|
+
"find_one",
|
|
865
|
+
"find",
|
|
866
|
+
"insert_one",
|
|
867
|
+
"insert_many",
|
|
868
|
+
"update_one",
|
|
869
|
+
"update_many",
|
|
870
|
+
"delete_one",
|
|
871
|
+
"delete_many",
|
|
872
|
+
"aggregate",
|
|
873
|
+
"count_documents",
|
|
874
|
+
"replace_one"
|
|
875
|
+
];
|
|
876
|
+
const parts = calledName.split(".");
|
|
877
|
+
const operation = parts[parts.length - 1];
|
|
878
|
+
if (mongoOps.includes(operation)) {
|
|
879
|
+
let collection = "unknown";
|
|
880
|
+
if (parts.length >= 2) {
|
|
881
|
+
collection = parts[parts.length - 2];
|
|
882
|
+
}
|
|
883
|
+
if (!analysis.databaseOperations) {
|
|
884
|
+
analysis.databaseOperations = [];
|
|
885
|
+
}
|
|
886
|
+
analysis.databaseOperations.push({
|
|
887
|
+
nodeId: parentId,
|
|
888
|
+
operation,
|
|
889
|
+
collection,
|
|
890
|
+
databaseType: "mongodb",
|
|
891
|
+
line: node.startPosition.row + 1
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
parseBodyForCalls(body, filePath, analysis, parentId) {
|
|
896
|
+
const walkForCalls = (node) => {
|
|
897
|
+
if (node.type === "call") {
|
|
898
|
+
this.handleCall(node, filePath, analysis, parentId);
|
|
899
|
+
} else if (node.type === "import_statement") {
|
|
900
|
+
this.handleImport(node, analysis);
|
|
901
|
+
} else if (node.type === "import_from_statement") {
|
|
902
|
+
this.handleFromImport(node, analysis);
|
|
903
|
+
} else if (node.type === "assignment") {
|
|
904
|
+
this.handleAssignment(node, filePath, analysis, parentId);
|
|
905
|
+
}
|
|
906
|
+
for (const child of node.children) {
|
|
907
|
+
walkForCalls(child);
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
walkForCalls(body);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Handle variable assignments to track types
|
|
914
|
+
* Examples:
|
|
915
|
+
* user = get_user(id) # Track if get_user has type hint
|
|
916
|
+
* handler = UserHandler(db) # Track: handler -> UserHandler
|
|
917
|
+
* org_handler = OrgHandler.get_instance() # Track: org_handler -> OrgHandler
|
|
918
|
+
*/
|
|
919
|
+
handleAssignment(node, filePath, analysis, scopeId) {
|
|
920
|
+
const leftNode = node.childForFieldName("left");
|
|
921
|
+
if (!leftNode || leftNode.type !== "identifier") return;
|
|
922
|
+
const variableName = leftNode.text;
|
|
923
|
+
const rightNode = node.childForFieldName("right");
|
|
924
|
+
if (!rightNode) return;
|
|
925
|
+
let typeName = null;
|
|
926
|
+
if (rightNode.type === "call") {
|
|
927
|
+
const funcNode = rightNode.childForFieldName("function");
|
|
928
|
+
if (funcNode) {
|
|
929
|
+
if (funcNode.type === "identifier") {
|
|
930
|
+
const funcName = funcNode.text;
|
|
931
|
+
if (funcName[0] === funcName[0].toUpperCase()) {
|
|
932
|
+
typeName = funcName;
|
|
933
|
+
} else {
|
|
934
|
+
typeName = `@call:${funcName}`;
|
|
935
|
+
}
|
|
936
|
+
} else if (funcNode.type === "attribute") {
|
|
937
|
+
const parts = funcNode.text.split(".");
|
|
938
|
+
if (parts.length >= 2) {
|
|
939
|
+
const className = parts[0];
|
|
940
|
+
const methodName = parts[1];
|
|
941
|
+
if (className[0] === className[0].toUpperCase()) {
|
|
942
|
+
typeName = className;
|
|
943
|
+
} else {
|
|
944
|
+
typeName = `@call:${funcNode.text}`;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (typeName && !analysis.variableTypes) {
|
|
951
|
+
analysis.variableTypes = [];
|
|
952
|
+
}
|
|
953
|
+
if (typeName) {
|
|
954
|
+
analysis.variableTypes.push({
|
|
955
|
+
variableName,
|
|
956
|
+
typeName,
|
|
957
|
+
scopeId,
|
|
958
|
+
line: node.startPosition.row + 1
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
|
|
964
|
+
// src/parsers/index.ts
|
|
965
|
+
function registerAllParsers() {
|
|
966
|
+
parserRegistry.register(new TypeScriptParser());
|
|
967
|
+
parserRegistry.register(new JavaScriptParser());
|
|
968
|
+
parserRegistry.register(new PythonParser());
|
|
969
|
+
}
|
|
970
|
+
registerAllParsers();
|
|
971
|
+
|
|
972
|
+
// src/flow/builder.ts
|
|
973
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
974
|
+
import { createHash } from "crypto";
|
|
975
|
+
import { glob } from "glob";
|
|
976
|
+
import { resolve, relative, extname } from "path";
|
|
977
|
+
|
|
978
|
+
// src/analysis/class-hierarchy.ts
|
|
979
|
+
function buildClassHierarchy(nodes) {
|
|
980
|
+
const classes = /* @__PURE__ */ new Map();
|
|
981
|
+
const children = /* @__PURE__ */ new Map();
|
|
982
|
+
const parents = /* @__PURE__ */ new Map();
|
|
983
|
+
for (const node of nodes) {
|
|
984
|
+
if (node.type === "class") {
|
|
985
|
+
const classNode = {
|
|
986
|
+
id: node.id,
|
|
987
|
+
name: node.name,
|
|
988
|
+
file: node.filePath,
|
|
989
|
+
line: node.startLine,
|
|
990
|
+
extends: node.extends || [],
|
|
991
|
+
methods: []
|
|
992
|
+
// Will populate in next step
|
|
993
|
+
};
|
|
994
|
+
classes.set(node.name, classNode);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
for (const node of nodes) {
|
|
998
|
+
if (node.type === "method" && node.parentClass) {
|
|
999
|
+
const classNode = classes.get(node.parentClass);
|
|
1000
|
+
if (classNode) {
|
|
1001
|
+
classNode.methods.push(node.name);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
for (const [className, classNode] of classes) {
|
|
1006
|
+
for (const parentName of classNode.extends) {
|
|
1007
|
+
if (!children.has(parentName)) {
|
|
1008
|
+
children.set(parentName, []);
|
|
1009
|
+
}
|
|
1010
|
+
children.get(parentName).push(className);
|
|
1011
|
+
if (!parents.has(className)) {
|
|
1012
|
+
parents.set(className, []);
|
|
1013
|
+
}
|
|
1014
|
+
parents.get(className).push(parentName);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
classes,
|
|
1019
|
+
children,
|
|
1020
|
+
parents
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
function findMethodDefinition(className, methodName, hierarchy) {
|
|
1024
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1025
|
+
const queue = [className];
|
|
1026
|
+
while (queue.length > 0) {
|
|
1027
|
+
const current = queue.shift();
|
|
1028
|
+
if (visited.has(current)) {
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
visited.add(current);
|
|
1032
|
+
const classNode = hierarchy.classes.get(current);
|
|
1033
|
+
if (classNode) {
|
|
1034
|
+
if (classNode.methods.includes(methodName)) {
|
|
1035
|
+
return classNode;
|
|
1036
|
+
}
|
|
1037
|
+
const parentNames = hierarchy.parents.get(current) || [];
|
|
1038
|
+
for (const parent of parentNames) {
|
|
1039
|
+
queue.push(parent);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
function resolveMethodCall(instanceType, methodName, hierarchy) {
|
|
1046
|
+
return findMethodDefinition(instanceType, methodName, hierarchy);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// src/flow/enhanced-resolver.ts
|
|
1050
|
+
var EnhancedResolver = class {
|
|
1051
|
+
constructor(storage) {
|
|
1052
|
+
this.storage = storage;
|
|
1053
|
+
}
|
|
1054
|
+
hierarchy = null;
|
|
1055
|
+
variableTypes = /* @__PURE__ */ new Map();
|
|
1056
|
+
/**
|
|
1057
|
+
* Build class hierarchy from all nodes in storage
|
|
1058
|
+
*/
|
|
1059
|
+
buildHierarchy() {
|
|
1060
|
+
const nodes = this.getAllNodes();
|
|
1061
|
+
this.hierarchy = buildClassHierarchy(nodes);
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Load variable type information from storage
|
|
1065
|
+
*/
|
|
1066
|
+
loadVariableTypes() {
|
|
1067
|
+
const stmt = this.storage.db.prepare(`
|
|
1068
|
+
SELECT * FROM variable_types
|
|
1069
|
+
`);
|
|
1070
|
+
const rows = stmt.all();
|
|
1071
|
+
for (const row of rows) {
|
|
1072
|
+
const scopeId = row.scope_node_id;
|
|
1073
|
+
if (!this.variableTypes.has(scopeId)) {
|
|
1074
|
+
this.variableTypes.set(scopeId, []);
|
|
1075
|
+
}
|
|
1076
|
+
this.variableTypes.get(scopeId).push({
|
|
1077
|
+
name: row.variable_name,
|
|
1078
|
+
type: row.type_name,
|
|
1079
|
+
scope: scopeId,
|
|
1080
|
+
line: row.line
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Resolve all unresolved edges using type information
|
|
1086
|
+
*/
|
|
1087
|
+
resolveAllCalls() {
|
|
1088
|
+
if (!this.hierarchy) {
|
|
1089
|
+
this.buildHierarchy();
|
|
1090
|
+
}
|
|
1091
|
+
this.loadVariableTypes();
|
|
1092
|
+
const resolvedCalls = [];
|
|
1093
|
+
const stmt = this.storage.db.prepare(`
|
|
1094
|
+
SELECT * FROM edges
|
|
1095
|
+
WHERE target_id LIKE 'ref:%'
|
|
1096
|
+
AND type = 'calls'
|
|
1097
|
+
`);
|
|
1098
|
+
const edges = stmt.all();
|
|
1099
|
+
for (const edgeRow of edges) {
|
|
1100
|
+
const edge = {
|
|
1101
|
+
id: edgeRow.id,
|
|
1102
|
+
type: edgeRow.type,
|
|
1103
|
+
sourceId: edgeRow.source_id,
|
|
1104
|
+
targetId: edgeRow.target_id,
|
|
1105
|
+
metadata: edgeRow.metadata ? JSON.parse(edgeRow.metadata) : void 0
|
|
1106
|
+
};
|
|
1107
|
+
const resolved = this.resolveCall(edge);
|
|
1108
|
+
if (resolved.targetNode) {
|
|
1109
|
+
resolvedCalls.push(resolved);
|
|
1110
|
+
this.updateEdgeTarget(edge.id, resolved.targetNode.id);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return resolvedCalls;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Resolve a single call edge
|
|
1117
|
+
*/
|
|
1118
|
+
resolveCall(edge) {
|
|
1119
|
+
const unresolvedName = edge.metadata?.unresolvedName;
|
|
1120
|
+
if (!unresolvedName) {
|
|
1121
|
+
return {
|
|
1122
|
+
originalEdge: edge,
|
|
1123
|
+
targetNode: null,
|
|
1124
|
+
confidence: "LOW",
|
|
1125
|
+
reason: "No unresolved name in metadata"
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
const sourceNode = this.storage.getNode(edge.sourceId);
|
|
1129
|
+
if (!sourceNode) {
|
|
1130
|
+
return {
|
|
1131
|
+
originalEdge: edge,
|
|
1132
|
+
targetNode: null,
|
|
1133
|
+
confidence: "LOW",
|
|
1134
|
+
reason: "Source node not found"
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
if (!unresolvedName.includes(".")) {
|
|
1138
|
+
return this.resolveSimpleCall(unresolvedName, sourceNode, edge);
|
|
1139
|
+
}
|
|
1140
|
+
return this.resolveMethodCall(unresolvedName, sourceNode, edge);
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Resolve simple function call (no dot notation)
|
|
1144
|
+
*/
|
|
1145
|
+
resolveSimpleCall(functionName, sourceNode, edge) {
|
|
1146
|
+
const sameFileNodes = this.storage.getNodesByFile(sourceNode.filePath);
|
|
1147
|
+
const localMatch = sameFileNodes.find(
|
|
1148
|
+
(n) => (n.type === "function" || n.type === "method") && n.name === functionName
|
|
1149
|
+
);
|
|
1150
|
+
if (localMatch) {
|
|
1151
|
+
return {
|
|
1152
|
+
originalEdge: edge,
|
|
1153
|
+
targetNode: localMatch,
|
|
1154
|
+
confidence: "HIGH",
|
|
1155
|
+
reason: "Found in same file"
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
const globalMatches = this.storage.searchNodesByName(functionName);
|
|
1159
|
+
if (globalMatches.length === 1) {
|
|
1160
|
+
return {
|
|
1161
|
+
originalEdge: edge,
|
|
1162
|
+
targetNode: globalMatches[0],
|
|
1163
|
+
confidence: "MEDIUM",
|
|
1164
|
+
reason: "Single global match"
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
if (globalMatches.length > 1) {
|
|
1168
|
+
const sourceDir = sourceNode.filePath.split("/").slice(0, -1).join("/");
|
|
1169
|
+
const sameDirMatch = globalMatches.find((n) => n.filePath.startsWith(sourceDir));
|
|
1170
|
+
if (sameDirMatch) {
|
|
1171
|
+
return {
|
|
1172
|
+
originalEdge: edge,
|
|
1173
|
+
targetNode: sameDirMatch,
|
|
1174
|
+
confidence: "MEDIUM",
|
|
1175
|
+
reason: "Multiple matches, picked same directory"
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
originalEdge: edge,
|
|
1180
|
+
targetNode: globalMatches[0],
|
|
1181
|
+
confidence: "LOW",
|
|
1182
|
+
reason: `Multiple matches (${globalMatches.length}), picked first`
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
return {
|
|
1186
|
+
originalEdge: edge,
|
|
1187
|
+
targetNode: null,
|
|
1188
|
+
confidence: "LOW",
|
|
1189
|
+
reason: "No matches found"
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Resolve method call using type information
|
|
1194
|
+
* Pattern: obj.method() or self.method()
|
|
1195
|
+
*/
|
|
1196
|
+
resolveMethodCall(fullName, sourceNode, edge) {
|
|
1197
|
+
const parts = fullName.split(".");
|
|
1198
|
+
const varName = parts[0];
|
|
1199
|
+
const methodName = parts.slice(1).join(".");
|
|
1200
|
+
if (varName === "self" || varName === "this") {
|
|
1201
|
+
return this.resolveSelfMethodCall(methodName, sourceNode, edge);
|
|
1202
|
+
}
|
|
1203
|
+
const scopeTypes = this.variableTypes.get(sourceNode.id) || [];
|
|
1204
|
+
const varType = scopeTypes.find((v) => v.name === varName);
|
|
1205
|
+
if (!varType) {
|
|
1206
|
+
return this.resolveSimpleCall(methodName, sourceNode, edge);
|
|
1207
|
+
}
|
|
1208
|
+
if (!this.hierarchy) {
|
|
1209
|
+
return {
|
|
1210
|
+
originalEdge: edge,
|
|
1211
|
+
targetNode: null,
|
|
1212
|
+
confidence: "LOW",
|
|
1213
|
+
reason: "Class hierarchy not built"
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
const classNode = resolveMethodCall(varType.type, methodName, this.hierarchy);
|
|
1217
|
+
if (classNode) {
|
|
1218
|
+
const methodNode = this.findMethodInClass(classNode.name, methodName);
|
|
1219
|
+
if (methodNode) {
|
|
1220
|
+
return {
|
|
1221
|
+
originalEdge: edge,
|
|
1222
|
+
targetNode: methodNode,
|
|
1223
|
+
confidence: "HIGH",
|
|
1224
|
+
reason: `Resolved via type tracking: ${varName}: ${varType.type}`
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return {
|
|
1229
|
+
originalEdge: edge,
|
|
1230
|
+
targetNode: null,
|
|
1231
|
+
confidence: "LOW",
|
|
1232
|
+
reason: `Type found (${varType.type}) but method not resolved`
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Resolve self.method() or this.method()
|
|
1237
|
+
*/
|
|
1238
|
+
resolveSelfMethodCall(methodName, sourceNode, edge) {
|
|
1239
|
+
const parentClass = sourceNode.metadata?.parentClass;
|
|
1240
|
+
if (!parentClass) {
|
|
1241
|
+
return {
|
|
1242
|
+
originalEdge: edge,
|
|
1243
|
+
targetNode: null,
|
|
1244
|
+
confidence: "LOW",
|
|
1245
|
+
reason: "self/this call but no parent class"
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
if (!this.hierarchy) {
|
|
1249
|
+
return {
|
|
1250
|
+
originalEdge: edge,
|
|
1251
|
+
targetNode: null,
|
|
1252
|
+
confidence: "LOW",
|
|
1253
|
+
reason: "Class hierarchy not built"
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
const classNode = resolveMethodCall(parentClass, methodName, this.hierarchy);
|
|
1257
|
+
if (classNode) {
|
|
1258
|
+
const methodNode2 = this.findMethodInClass(classNode.name, methodName);
|
|
1259
|
+
if (methodNode2) {
|
|
1260
|
+
return {
|
|
1261
|
+
originalEdge: edge,
|
|
1262
|
+
targetNode: methodNode2,
|
|
1263
|
+
confidence: "HIGH",
|
|
1264
|
+
reason: `Resolved self.${methodName} in ${parentClass}`
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const sameFileNodes = this.storage.getNodesByFile(sourceNode.filePath);
|
|
1269
|
+
const methodNode = sameFileNodes.find(
|
|
1270
|
+
(n) => n.type === "method" && n.name === methodName
|
|
1271
|
+
);
|
|
1272
|
+
if (methodNode) {
|
|
1273
|
+
return {
|
|
1274
|
+
originalEdge: edge,
|
|
1275
|
+
targetNode: methodNode,
|
|
1276
|
+
confidence: "MEDIUM",
|
|
1277
|
+
reason: "Found method in same file"
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
originalEdge: edge,
|
|
1282
|
+
targetNode: null,
|
|
1283
|
+
confidence: "LOW",
|
|
1284
|
+
reason: `Method ${methodName} not found in ${parentClass}`
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Find method node in a class
|
|
1289
|
+
*/
|
|
1290
|
+
findMethodInClass(className, methodName) {
|
|
1291
|
+
const stmt = this.storage.db.prepare(`
|
|
1292
|
+
SELECT * FROM nodes
|
|
1293
|
+
WHERE type = 'method'
|
|
1294
|
+
AND name = ?
|
|
1295
|
+
AND json_extract(metadata, '$.parentClass') = ?
|
|
1296
|
+
`);
|
|
1297
|
+
const row = stmt.get(methodName, className);
|
|
1298
|
+
if (!row) return null;
|
|
1299
|
+
return {
|
|
1300
|
+
id: row.id,
|
|
1301
|
+
type: row.type,
|
|
1302
|
+
name: row.name,
|
|
1303
|
+
filePath: row.file_path,
|
|
1304
|
+
startLine: row.start_line,
|
|
1305
|
+
endLine: row.end_line,
|
|
1306
|
+
language: row.language,
|
|
1307
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Get all nodes from storage
|
|
1312
|
+
*/
|
|
1313
|
+
getAllNodes() {
|
|
1314
|
+
const stmt = this.storage.db.prepare("SELECT * FROM nodes");
|
|
1315
|
+
const rows = stmt.all();
|
|
1316
|
+
return rows.map((row) => ({
|
|
1317
|
+
id: row.id,
|
|
1318
|
+
type: row.type,
|
|
1319
|
+
name: row.name,
|
|
1320
|
+
filePath: row.file_path,
|
|
1321
|
+
startLine: row.start_line,
|
|
1322
|
+
endLine: row.end_line,
|
|
1323
|
+
language: row.language,
|
|
1324
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
1325
|
+
}));
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Update edge target in database
|
|
1329
|
+
*/
|
|
1330
|
+
updateEdgeTarget(edgeId, newTargetId) {
|
|
1331
|
+
const stmt = this.storage.db.prepare(`
|
|
1332
|
+
UPDATE edges
|
|
1333
|
+
SET target_id = ?
|
|
1334
|
+
WHERE id = ?
|
|
1335
|
+
`);
|
|
1336
|
+
stmt.run(newTargetId, edgeId);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Get resolution statistics
|
|
1340
|
+
*/
|
|
1341
|
+
getStats() {
|
|
1342
|
+
const totalStmt = this.storage.db.prepare(`
|
|
1343
|
+
SELECT COUNT(*) as count FROM edges WHERE type = 'calls'
|
|
1344
|
+
`);
|
|
1345
|
+
const totalRow = totalStmt.get();
|
|
1346
|
+
const totalCalls = totalRow.count;
|
|
1347
|
+
const resolvedStmt = this.storage.db.prepare(`
|
|
1348
|
+
SELECT COUNT(*) as count FROM edges
|
|
1349
|
+
WHERE type = 'calls' AND target_id NOT LIKE 'ref:%'
|
|
1350
|
+
`);
|
|
1351
|
+
const resolvedRow = resolvedStmt.get();
|
|
1352
|
+
const resolvedCalls = resolvedRow.count;
|
|
1353
|
+
const unresolvedCalls = totalCalls - resolvedCalls;
|
|
1354
|
+
const resolutionRate = totalCalls > 0 ? resolvedCalls / totalCalls * 100 : 0;
|
|
1355
|
+
return {
|
|
1356
|
+
totalCalls,
|
|
1357
|
+
resolvedCalls,
|
|
1358
|
+
unresolvedCalls,
|
|
1359
|
+
resolutionRate
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
// src/flow/phase3-analyzer.ts
|
|
1365
|
+
import { readFileSync } from "fs";
|
|
1366
|
+
|
|
1367
|
+
// src/analysis/property-tracker.ts
|
|
1368
|
+
function extractPropertyAccesses(functionNode, scopeId, scopeName, filePath) {
|
|
1369
|
+
const accesses = [];
|
|
1370
|
+
const isJS = /\.(js|ts|jsx|tsx)$/.test(filePath);
|
|
1371
|
+
const isPython = /\.py$/.test(filePath);
|
|
1372
|
+
if (isJS) {
|
|
1373
|
+
extractJavaScriptPropertyAccesses(functionNode, scopeId, scopeName, accesses);
|
|
1374
|
+
} else if (isPython) {
|
|
1375
|
+
extractPythonPropertyAccesses(functionNode, scopeId, scopeName, accesses);
|
|
1376
|
+
}
|
|
1377
|
+
return accesses;
|
|
1378
|
+
}
|
|
1379
|
+
function extractJavaScriptPropertyAccesses(node, scopeId, scopeName, accesses) {
|
|
1380
|
+
traverseNode(node, (current) => {
|
|
1381
|
+
if (current.type === "member_expression") {
|
|
1382
|
+
const access = parseJavaScriptMemberExpression(current, scopeId);
|
|
1383
|
+
if (access) {
|
|
1384
|
+
accesses.push(access);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
if (current.type === "optional_chain") {
|
|
1388
|
+
const memberExpr = current.children.find((c) => c.type === "member_expression");
|
|
1389
|
+
if (memberExpr) {
|
|
1390
|
+
const access = parseJavaScriptMemberExpression(memberExpr, scopeId);
|
|
1391
|
+
if (access) {
|
|
1392
|
+
access.accessType = "optional";
|
|
1393
|
+
accesses.push(access);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (current.type === "subscript_expression") {
|
|
1398
|
+
const access = parseJavaScriptSubscript(current, scopeId);
|
|
1399
|
+
if (access) {
|
|
1400
|
+
accesses.push(access);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
function parseJavaScriptMemberExpression(node, scopeId) {
|
|
1406
|
+
const propertyPath = [];
|
|
1407
|
+
let objectName = "";
|
|
1408
|
+
let currentNode = node;
|
|
1409
|
+
while (currentNode && currentNode.type === "member_expression") {
|
|
1410
|
+
const propertyNode = currentNode.childForFieldName("property");
|
|
1411
|
+
if (propertyNode) {
|
|
1412
|
+
propertyPath.unshift(propertyNode.text);
|
|
1413
|
+
}
|
|
1414
|
+
const objectNode = currentNode.childForFieldName("object");
|
|
1415
|
+
if (!objectNode) break;
|
|
1416
|
+
if (objectNode.type === "identifier") {
|
|
1417
|
+
objectName = objectNode.text;
|
|
1418
|
+
break;
|
|
1419
|
+
} else if (objectNode.type === "member_expression") {
|
|
1420
|
+
currentNode = objectNode;
|
|
1421
|
+
} else {
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (!objectName || isCommonJavaScriptObject(objectName)) {
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
if (propertyPath.length === 0) {
|
|
1429
|
+
return null;
|
|
1430
|
+
}
|
|
1431
|
+
return {
|
|
1432
|
+
objectName,
|
|
1433
|
+
propertyPath,
|
|
1434
|
+
fullPath: `${objectName}.${propertyPath.join(".")}`,
|
|
1435
|
+
line: node.startPosition.row + 1,
|
|
1436
|
+
scopeId,
|
|
1437
|
+
accessType: "dot"
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
function parseJavaScriptSubscript(node, scopeId) {
|
|
1441
|
+
const objectNode = node.childForFieldName("object");
|
|
1442
|
+
const indexNode = node.childForFieldName("index");
|
|
1443
|
+
if (!objectNode || !indexNode) return null;
|
|
1444
|
+
if (indexNode.type !== "string") return null;
|
|
1445
|
+
const objectName = objectNode.text;
|
|
1446
|
+
const propertyName = indexNode.text.replace(/['"]/g, "");
|
|
1447
|
+
if (isCommonJavaScriptObject(objectName)) {
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
return {
|
|
1451
|
+
objectName,
|
|
1452
|
+
propertyPath: [propertyName],
|
|
1453
|
+
fullPath: `${objectName}.${propertyName}`,
|
|
1454
|
+
line: node.startPosition.row + 1,
|
|
1455
|
+
scopeId,
|
|
1456
|
+
accessType: "bracket"
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
function extractPythonPropertyAccesses(node, scopeId, scopeName, accesses) {
|
|
1460
|
+
traverseNode(node, (current) => {
|
|
1461
|
+
if (current.type === "attribute") {
|
|
1462
|
+
const access = parsePythonAttribute(current, scopeId);
|
|
1463
|
+
if (access) {
|
|
1464
|
+
accesses.push(access);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
if (current.type === "subscript") {
|
|
1468
|
+
const access = parsePythonSubscript(current, scopeId);
|
|
1469
|
+
if (access) {
|
|
1470
|
+
accesses.push(access);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1475
|
+
function parsePythonAttribute(node, scopeId) {
|
|
1476
|
+
const propertyPath = [];
|
|
1477
|
+
let objectName = "";
|
|
1478
|
+
let currentNode = node;
|
|
1479
|
+
while (currentNode && currentNode.type === "attribute") {
|
|
1480
|
+
const attrNode = currentNode.childForFieldName("attribute");
|
|
1481
|
+
if (attrNode) {
|
|
1482
|
+
propertyPath.unshift(attrNode.text);
|
|
1483
|
+
}
|
|
1484
|
+
const objectNode = currentNode.childForFieldName("object");
|
|
1485
|
+
if (!objectNode) break;
|
|
1486
|
+
if (objectNode.type === "identifier") {
|
|
1487
|
+
objectName = objectNode.text;
|
|
1488
|
+
break;
|
|
1489
|
+
} else if (objectNode.type === "attribute") {
|
|
1490
|
+
currentNode = objectNode;
|
|
1491
|
+
} else {
|
|
1492
|
+
return null;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
if (!objectName || isCommonPythonObject(objectName)) {
|
|
1496
|
+
return null;
|
|
1497
|
+
}
|
|
1498
|
+
if (propertyPath.length === 0) {
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
return {
|
|
1502
|
+
objectName,
|
|
1503
|
+
propertyPath,
|
|
1504
|
+
fullPath: `${objectName}.${propertyPath.join(".")}`,
|
|
1505
|
+
line: node.startPosition.row + 1,
|
|
1506
|
+
scopeId,
|
|
1507
|
+
accessType: "dot"
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
function parsePythonSubscript(node, scopeId) {
|
|
1511
|
+
const valueNode = node.childForFieldName("value");
|
|
1512
|
+
const subscriptChildren = node.children.filter(
|
|
1513
|
+
(c) => c.type === "string" || c.type === "identifier"
|
|
1514
|
+
);
|
|
1515
|
+
if (!valueNode || subscriptChildren.length === 0) return null;
|
|
1516
|
+
const keyNode = subscriptChildren.find((c) => c.type === "string");
|
|
1517
|
+
if (!keyNode) return null;
|
|
1518
|
+
const objectName = valueNode.text;
|
|
1519
|
+
const stringContent = keyNode.children.find((c) => c.type === "string_content");
|
|
1520
|
+
const propertyName = stringContent ? stringContent.text : keyNode.text.replace(/['"]/g, "");
|
|
1521
|
+
if (isCommonPythonObject(objectName)) {
|
|
1522
|
+
return null;
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
objectName,
|
|
1526
|
+
propertyPath: [propertyName],
|
|
1527
|
+
fullPath: `${objectName}.${propertyName}`,
|
|
1528
|
+
line: node.startPosition.row + 1,
|
|
1529
|
+
scopeId,
|
|
1530
|
+
accessType: "bracket"
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
function traverseNode(node, visitor) {
|
|
1534
|
+
visitor(node);
|
|
1535
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1536
|
+
const child = node.child(i);
|
|
1537
|
+
if (child) {
|
|
1538
|
+
traverseNode(child, visitor);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
function isCommonJavaScriptObject(name) {
|
|
1543
|
+
const commonObjects = /* @__PURE__ */ new Set([
|
|
1544
|
+
// Global objects
|
|
1545
|
+
"console",
|
|
1546
|
+
"window",
|
|
1547
|
+
"document",
|
|
1548
|
+
"process",
|
|
1549
|
+
"global",
|
|
1550
|
+
// Common libraries
|
|
1551
|
+
"Math",
|
|
1552
|
+
"JSON",
|
|
1553
|
+
"Date",
|
|
1554
|
+
"Array",
|
|
1555
|
+
"Object",
|
|
1556
|
+
"String",
|
|
1557
|
+
"Number",
|
|
1558
|
+
// Keywords
|
|
1559
|
+
"this",
|
|
1560
|
+
"super",
|
|
1561
|
+
// Common module patterns
|
|
1562
|
+
"exports",
|
|
1563
|
+
"module",
|
|
1564
|
+
"require"
|
|
1565
|
+
]);
|
|
1566
|
+
return commonObjects.has(name);
|
|
1567
|
+
}
|
|
1568
|
+
function isCommonPythonObject(name) {
|
|
1569
|
+
const commonObjects = /* @__PURE__ */ new Set([
|
|
1570
|
+
// Keywords
|
|
1571
|
+
"self",
|
|
1572
|
+
"cls",
|
|
1573
|
+
// Built-in types
|
|
1574
|
+
"str",
|
|
1575
|
+
"int",
|
|
1576
|
+
"float",
|
|
1577
|
+
"dict",
|
|
1578
|
+
"list",
|
|
1579
|
+
"tuple",
|
|
1580
|
+
"set",
|
|
1581
|
+
// Common modules
|
|
1582
|
+
"os",
|
|
1583
|
+
"sys",
|
|
1584
|
+
"json",
|
|
1585
|
+
"math",
|
|
1586
|
+
"datetime"
|
|
1587
|
+
]);
|
|
1588
|
+
return commonObjects.has(name);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// src/analysis/return-analyzer.ts
|
|
1592
|
+
function analyzeReturnStatements(functionNode, functionId, functionName, filePath) {
|
|
1593
|
+
const returns = [];
|
|
1594
|
+
const isJS = /\.(js|ts|jsx|tsx)$/.test(filePath);
|
|
1595
|
+
const isPython = /\.py$/.test(filePath);
|
|
1596
|
+
if (isJS) {
|
|
1597
|
+
extractJavaScriptReturns(functionNode, functionId, functionName, returns);
|
|
1598
|
+
} else if (isPython) {
|
|
1599
|
+
extractPythonReturns(functionNode, functionId, functionName, returns);
|
|
1600
|
+
}
|
|
1601
|
+
return returns;
|
|
1602
|
+
}
|
|
1603
|
+
function extractJavaScriptReturns(node, functionId, functionName, returns) {
|
|
1604
|
+
traverseNode2(node, (current) => {
|
|
1605
|
+
if (current.type === "return_statement") {
|
|
1606
|
+
const returnNode = current.children.find(
|
|
1607
|
+
(c) => c.type !== "return" && c.type !== ";"
|
|
1608
|
+
);
|
|
1609
|
+
if (returnNode) {
|
|
1610
|
+
const returnInfo = parseJavaScriptReturnValue(
|
|
1611
|
+
returnNode,
|
|
1612
|
+
functionId,
|
|
1613
|
+
functionName
|
|
1614
|
+
);
|
|
1615
|
+
if (returnInfo) {
|
|
1616
|
+
returns.push(returnInfo);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
function parseJavaScriptReturnValue(returnNode, functionId, functionName) {
|
|
1623
|
+
const line = returnNode.startPosition.row + 1;
|
|
1624
|
+
const returnExpression = returnNode.text;
|
|
1625
|
+
let returnType = null;
|
|
1626
|
+
let objectShape = null;
|
|
1627
|
+
if (returnNode.type === "object") {
|
|
1628
|
+
objectShape = extractJavaScriptObjectShape(returnNode);
|
|
1629
|
+
returnType = "object";
|
|
1630
|
+
} else if (returnNode.type === "array") {
|
|
1631
|
+
returnType = "array";
|
|
1632
|
+
objectShape = {
|
|
1633
|
+
properties: [],
|
|
1634
|
+
propertyTypes: /* @__PURE__ */ new Map(),
|
|
1635
|
+
nestedShapes: /* @__PURE__ */ new Map(),
|
|
1636
|
+
isArray: true,
|
|
1637
|
+
isDictionary: false,
|
|
1638
|
+
isOptional: false
|
|
1639
|
+
};
|
|
1640
|
+
} else if (returnNode.type === "new_expression") {
|
|
1641
|
+
const constructorNode = returnNode.childForFieldName("constructor");
|
|
1642
|
+
if (constructorNode) {
|
|
1643
|
+
returnType = constructorNode.text;
|
|
1644
|
+
}
|
|
1645
|
+
} else if (returnNode.type === "identifier") {
|
|
1646
|
+
returnType = `@var:${returnNode.text}`;
|
|
1647
|
+
} else if (returnNode.type === "call_expression") {
|
|
1648
|
+
const funcNode = returnNode.childForFieldName("function");
|
|
1649
|
+
if (funcNode) {
|
|
1650
|
+
returnType = `@call:${funcNode.text}`;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
return {
|
|
1654
|
+
functionId,
|
|
1655
|
+
functionName,
|
|
1656
|
+
returnType,
|
|
1657
|
+
objectShape,
|
|
1658
|
+
line,
|
|
1659
|
+
returnExpression
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
function extractJavaScriptObjectShape(objectNode) {
|
|
1663
|
+
const properties = [];
|
|
1664
|
+
const propertyTypes = /* @__PURE__ */ new Map();
|
|
1665
|
+
const nestedShapes = /* @__PURE__ */ new Map();
|
|
1666
|
+
for (const child of objectNode.children) {
|
|
1667
|
+
if (child.type === "pair") {
|
|
1668
|
+
const keyNode = child.childForFieldName("key");
|
|
1669
|
+
const valueNode = child.childForFieldName("value");
|
|
1670
|
+
if (keyNode) {
|
|
1671
|
+
const propertyName = keyNode.text.replace(/['"]/g, "");
|
|
1672
|
+
properties.push(propertyName);
|
|
1673
|
+
if (valueNode) {
|
|
1674
|
+
const propertyType = inferJavaScriptType(valueNode);
|
|
1675
|
+
propertyTypes.set(propertyName, propertyType);
|
|
1676
|
+
if (valueNode.type === "object") {
|
|
1677
|
+
const nestedShape = extractJavaScriptObjectShape(valueNode);
|
|
1678
|
+
nestedShapes.set(propertyName, nestedShape);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return {
|
|
1685
|
+
properties,
|
|
1686
|
+
propertyTypes,
|
|
1687
|
+
nestedShapes,
|
|
1688
|
+
isArray: false,
|
|
1689
|
+
isDictionary: false,
|
|
1690
|
+
isOptional: false
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
function inferJavaScriptType(valueNode) {
|
|
1694
|
+
switch (valueNode.type) {
|
|
1695
|
+
case "number":
|
|
1696
|
+
return "number";
|
|
1697
|
+
case "string":
|
|
1698
|
+
case "template_string":
|
|
1699
|
+
return "string";
|
|
1700
|
+
case "true":
|
|
1701
|
+
case "false":
|
|
1702
|
+
return "boolean";
|
|
1703
|
+
case "null":
|
|
1704
|
+
return "null";
|
|
1705
|
+
case "undefined":
|
|
1706
|
+
return "undefined";
|
|
1707
|
+
case "array":
|
|
1708
|
+
return "array";
|
|
1709
|
+
case "object":
|
|
1710
|
+
return "object";
|
|
1711
|
+
case "arrow_function":
|
|
1712
|
+
case "function_expression":
|
|
1713
|
+
return "function";
|
|
1714
|
+
case "identifier":
|
|
1715
|
+
return `@var:${valueNode.text}`;
|
|
1716
|
+
case "call_expression":
|
|
1717
|
+
return "@call";
|
|
1718
|
+
default:
|
|
1719
|
+
return "unknown";
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
function extractPythonReturns(node, functionId, functionName, returns) {
|
|
1723
|
+
traverseNode2(node, (current) => {
|
|
1724
|
+
if (current.type === "return_statement") {
|
|
1725
|
+
const returnNode = current.children.find((c) => c.type !== "return");
|
|
1726
|
+
if (returnNode) {
|
|
1727
|
+
const returnInfo = parsePythonReturnValue(
|
|
1728
|
+
returnNode,
|
|
1729
|
+
functionId,
|
|
1730
|
+
functionName
|
|
1731
|
+
);
|
|
1732
|
+
if (returnInfo) {
|
|
1733
|
+
returns.push(returnInfo);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1739
|
+
function parsePythonReturnValue(returnNode, functionId, functionName) {
|
|
1740
|
+
const line = returnNode.startPosition.row + 1;
|
|
1741
|
+
const returnExpression = returnNode.text;
|
|
1742
|
+
let returnType = null;
|
|
1743
|
+
let objectShape = null;
|
|
1744
|
+
if (returnNode.type === "dictionary") {
|
|
1745
|
+
objectShape = extractPythonDictionaryShape(returnNode);
|
|
1746
|
+
returnType = "dict";
|
|
1747
|
+
} else if (returnNode.type === "list") {
|
|
1748
|
+
returnType = "list";
|
|
1749
|
+
objectShape = {
|
|
1750
|
+
properties: [],
|
|
1751
|
+
propertyTypes: /* @__PURE__ */ new Map(),
|
|
1752
|
+
nestedShapes: /* @__PURE__ */ new Map(),
|
|
1753
|
+
isArray: true,
|
|
1754
|
+
isDictionary: false,
|
|
1755
|
+
isOptional: false
|
|
1756
|
+
};
|
|
1757
|
+
} else if (returnNode.type === "call") {
|
|
1758
|
+
const funcNode = returnNode.childForFieldName("function");
|
|
1759
|
+
if (funcNode) {
|
|
1760
|
+
const funcName = funcNode.text;
|
|
1761
|
+
if (funcName[0] === funcName[0].toUpperCase()) {
|
|
1762
|
+
returnType = funcName;
|
|
1763
|
+
} else {
|
|
1764
|
+
returnType = `@call:${funcName}`;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
} else if (returnNode.type === "identifier") {
|
|
1768
|
+
returnType = `@var:${returnNode.text}`;
|
|
1769
|
+
} else if (returnNode.type === "list_comprehension") {
|
|
1770
|
+
returnType = "list";
|
|
1771
|
+
objectShape = {
|
|
1772
|
+
properties: [],
|
|
1773
|
+
propertyTypes: /* @__PURE__ */ new Map(),
|
|
1774
|
+
nestedShapes: /* @__PURE__ */ new Map(),
|
|
1775
|
+
isArray: true,
|
|
1776
|
+
isDictionary: false,
|
|
1777
|
+
isOptional: false
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
return {
|
|
1781
|
+
functionId,
|
|
1782
|
+
functionName,
|
|
1783
|
+
returnType,
|
|
1784
|
+
objectShape,
|
|
1785
|
+
line,
|
|
1786
|
+
returnExpression
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
function extractPythonDictionaryShape(dictNode) {
|
|
1790
|
+
const properties = [];
|
|
1791
|
+
const propertyTypes = /* @__PURE__ */ new Map();
|
|
1792
|
+
const nestedShapes = /* @__PURE__ */ new Map();
|
|
1793
|
+
for (const child of dictNode.children) {
|
|
1794
|
+
if (child.type === "pair") {
|
|
1795
|
+
const keyNode = child.children.find((c) => c.type === "string");
|
|
1796
|
+
const valueNode = child.children.find(
|
|
1797
|
+
(c) => c.type !== "string" && c.type !== ":"
|
|
1798
|
+
);
|
|
1799
|
+
if (keyNode) {
|
|
1800
|
+
const stringContent = keyNode.children.find(
|
|
1801
|
+
(c) => c.type === "string_content"
|
|
1802
|
+
);
|
|
1803
|
+
const propertyName = stringContent ? stringContent.text : keyNode.text.replace(/['"]/g, "");
|
|
1804
|
+
properties.push(propertyName);
|
|
1805
|
+
if (valueNode) {
|
|
1806
|
+
const propertyType = inferPythonType(valueNode);
|
|
1807
|
+
propertyTypes.set(propertyName, propertyType);
|
|
1808
|
+
if (valueNode.type === "dictionary") {
|
|
1809
|
+
const nestedShape = extractPythonDictionaryShape(valueNode);
|
|
1810
|
+
nestedShapes.set(propertyName, nestedShape);
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
return {
|
|
1817
|
+
properties,
|
|
1818
|
+
propertyTypes,
|
|
1819
|
+
nestedShapes,
|
|
1820
|
+
isArray: false,
|
|
1821
|
+
isDictionary: true,
|
|
1822
|
+
isOptional: false
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
function inferPythonType(valueNode) {
|
|
1826
|
+
switch (valueNode.type) {
|
|
1827
|
+
case "integer":
|
|
1828
|
+
case "float":
|
|
1829
|
+
return "number";
|
|
1830
|
+
case "string":
|
|
1831
|
+
return "string";
|
|
1832
|
+
case "true":
|
|
1833
|
+
case "false":
|
|
1834
|
+
return "boolean";
|
|
1835
|
+
case "none":
|
|
1836
|
+
return "None";
|
|
1837
|
+
case "list":
|
|
1838
|
+
return "list";
|
|
1839
|
+
case "dictionary":
|
|
1840
|
+
return "dict";
|
|
1841
|
+
case "lambda":
|
|
1842
|
+
return "function";
|
|
1843
|
+
case "identifier":
|
|
1844
|
+
return `@var:${valueNode.text}`;
|
|
1845
|
+
case "call":
|
|
1846
|
+
return "@call";
|
|
1847
|
+
default:
|
|
1848
|
+
return "unknown";
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
function traverseNode2(node, visitor) {
|
|
1852
|
+
visitor(node);
|
|
1853
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1854
|
+
const child = node.child(i);
|
|
1855
|
+
if (child) {
|
|
1856
|
+
traverseNode2(child, visitor);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// src/flow/storage-queries-phase3.ts
|
|
1862
|
+
import { randomUUID } from "crypto";
|
|
1863
|
+
function storePropertyAccesses(db, accesses) {
|
|
1864
|
+
const stmt = db.prepare(`
|
|
1865
|
+
INSERT OR REPLACE INTO property_accesses
|
|
1866
|
+
(id, scope_node_id, object_name, property_path, full_path, access_type, file_path, line)
|
|
1867
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1868
|
+
`);
|
|
1869
|
+
const insert = db.transaction((accesses2) => {
|
|
1870
|
+
for (const access of accesses2) {
|
|
1871
|
+
stmt.run(
|
|
1872
|
+
randomUUID(),
|
|
1873
|
+
access.scopeId,
|
|
1874
|
+
access.objectName,
|
|
1875
|
+
JSON.stringify(access.propertyPath),
|
|
1876
|
+
access.fullPath,
|
|
1877
|
+
access.accessType,
|
|
1878
|
+
access.filePath || "",
|
|
1879
|
+
access.line
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
});
|
|
1883
|
+
insert(accesses);
|
|
1884
|
+
}
|
|
1885
|
+
function storeReturnInfo(db, returns) {
|
|
1886
|
+
const stmt = db.prepare(`
|
|
1887
|
+
INSERT OR REPLACE INTO return_shapes
|
|
1888
|
+
(id, function_id, return_type, object_shape, line, return_expression)
|
|
1889
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1890
|
+
`);
|
|
1891
|
+
const insert = db.transaction((returns2) => {
|
|
1892
|
+
for (const ret of returns2) {
|
|
1893
|
+
stmt.run(
|
|
1894
|
+
randomUUID(),
|
|
1895
|
+
ret.functionId,
|
|
1896
|
+
ret.returnType,
|
|
1897
|
+
ret.objectShape ? JSON.stringify(serializeObjectShape(ret.objectShape)) : null,
|
|
1898
|
+
ret.line,
|
|
1899
|
+
ret.returnExpression
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1903
|
+
insert(returns);
|
|
1904
|
+
}
|
|
1905
|
+
function serializeObjectShape(shape) {
|
|
1906
|
+
return {
|
|
1907
|
+
properties: shape.properties,
|
|
1908
|
+
propertyTypes: Array.from(shape.propertyTypes.entries()),
|
|
1909
|
+
nestedShapes: Array.from(shape.nestedShapes.entries()).map(([key, nested]) => [
|
|
1910
|
+
key,
|
|
1911
|
+
serializeObjectShape(nested)
|
|
1912
|
+
]),
|
|
1913
|
+
isArray: shape.isArray,
|
|
1914
|
+
isDictionary: shape.isDictionary,
|
|
1915
|
+
isOptional: shape.isOptional
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
// src/flow/phase3-analyzer.ts
|
|
1920
|
+
function analyzeFileForPhase3(filePath, functionNodes, db) {
|
|
1921
|
+
try {
|
|
1922
|
+
const language = getLanguageFromPath(filePath);
|
|
1923
|
+
if (!language) return;
|
|
1924
|
+
const parserInstance = parserRegistry.getByLanguage(language);
|
|
1925
|
+
if (!parserInstance || !parserInstance.treeSitterParser) return;
|
|
1926
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1927
|
+
const tree = parserInstance.treeSitterParser.parse(content);
|
|
1928
|
+
const rootNode = tree.rootNode;
|
|
1929
|
+
for (const func of functionNodes) {
|
|
1930
|
+
const funcNode = findNodeAtLine(rootNode, func.startLine, func.endLine);
|
|
1931
|
+
if (!funcNode) continue;
|
|
1932
|
+
const accesses = extractPropertyAccesses(funcNode, func.id, func.name, filePath);
|
|
1933
|
+
if (accesses.length > 0) {
|
|
1934
|
+
const accessesWithFile = accesses.map((a) => ({ ...a, filePath }));
|
|
1935
|
+
storePropertyAccesses(db, accessesWithFile);
|
|
1936
|
+
}
|
|
1937
|
+
const returns = analyzeReturnStatements(funcNode, func.id, func.name, filePath);
|
|
1938
|
+
if (returns.length > 0) {
|
|
1939
|
+
storeReturnInfo(db, returns);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
function getLanguageFromPath(filePath) {
|
|
1946
|
+
if (filePath.endsWith(".py")) return "python";
|
|
1947
|
+
if (filePath.endsWith(".js") || filePath.endsWith(".jsx")) return "javascript";
|
|
1948
|
+
if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "typescript";
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1951
|
+
function findNodeAtLine(node, startLine, endLine) {
|
|
1952
|
+
const nodeStart = node.startPosition.row + 1;
|
|
1953
|
+
const nodeEnd = node.endPosition.row + 1;
|
|
1954
|
+
if (nodeStart === startLine && nodeEnd === endLine) {
|
|
1955
|
+
return node;
|
|
1956
|
+
}
|
|
1957
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1958
|
+
const child = node.child(i);
|
|
1959
|
+
if (child) {
|
|
1960
|
+
const result = findNodeAtLine(child, startLine, endLine);
|
|
1961
|
+
if (result) return result;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
if (nodeStart <= startLine && nodeEnd >= endLine) {
|
|
1965
|
+
if (node.type === "function_definition" || node.type === "function_declaration" || node.type === "method_definition" || node.type === "arrow_function" || node.type === "function_expression") {
|
|
1966
|
+
return node;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/flow/builder.ts
|
|
1973
|
+
var FlowBuilder = class {
|
|
1974
|
+
storage;
|
|
1975
|
+
config;
|
|
1976
|
+
onProgress;
|
|
1977
|
+
moduleMap = /* @__PURE__ */ new Map();
|
|
1978
|
+
// file path → exports
|
|
1979
|
+
errors = [];
|
|
1980
|
+
constructor(storage, config) {
|
|
1981
|
+
this.storage = storage;
|
|
1982
|
+
this.config = config;
|
|
1983
|
+
}
|
|
1984
|
+
setProgressCallback(callback) {
|
|
1985
|
+
this.onProgress = callback;
|
|
1986
|
+
}
|
|
1987
|
+
async build() {
|
|
1988
|
+
const rootPath = resolve(this.config.rootPath);
|
|
1989
|
+
this.emitProgress({
|
|
1990
|
+
phase: "discovering",
|
|
1991
|
+
total: 0,
|
|
1992
|
+
current: 0,
|
|
1993
|
+
currentFile: ""
|
|
1994
|
+
});
|
|
1995
|
+
const allFiles = await this.discoverFiles(rootPath);
|
|
1996
|
+
const filesToIndex = [];
|
|
1997
|
+
let skipped = 0;
|
|
1998
|
+
for (const filePath of allFiles) {
|
|
1999
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
2000
|
+
const hash = this.hashContent(content);
|
|
2001
|
+
if (!this.config.forceReindex) {
|
|
2002
|
+
const existingHash = this.storage.getFileHash(filePath);
|
|
2003
|
+
if (existingHash === hash) {
|
|
2004
|
+
skipped++;
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
filesToIndex.push({ path: filePath, hash });
|
|
2009
|
+
}
|
|
2010
|
+
let indexed = 0;
|
|
2011
|
+
for (const file of filesToIndex) {
|
|
2012
|
+
this.emitProgress({
|
|
2013
|
+
phase: "parsing",
|
|
2014
|
+
total: filesToIndex.length,
|
|
2015
|
+
current: indexed + 1,
|
|
2016
|
+
currentFile: relative(rootPath, file.path)
|
|
2017
|
+
});
|
|
2018
|
+
try {
|
|
2019
|
+
await this.parseAndStore(file.path, file.hash, rootPath);
|
|
2020
|
+
indexed++;
|
|
2021
|
+
} catch (error) {
|
|
2022
|
+
this.errors.push(`${relative(rootPath, file.path)}: ${error}`);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
this.emitProgress({
|
|
2026
|
+
phase: "resolving",
|
|
2027
|
+
total: filesToIndex.length,
|
|
2028
|
+
current: filesToIndex.length,
|
|
2029
|
+
currentFile: ""
|
|
2030
|
+
});
|
|
2031
|
+
this.resolveAllCalls();
|
|
2032
|
+
const enhancedResolver = new EnhancedResolver(this.storage);
|
|
2033
|
+
enhancedResolver.buildHierarchy();
|
|
2034
|
+
const enhancedResolved = enhancedResolver.resolveAllCalls();
|
|
2035
|
+
if (enhancedResolved.length > 0) {
|
|
2036
|
+
console.log(`\u2728 Enhanced resolver: resolved ${enhancedResolved.length} additional calls`);
|
|
2037
|
+
}
|
|
2038
|
+
const stats = this.storage.getStats();
|
|
2039
|
+
this.emitProgress({
|
|
2040
|
+
phase: "complete",
|
|
2041
|
+
total: filesToIndex.length,
|
|
2042
|
+
current: filesToIndex.length,
|
|
2043
|
+
currentFile: ""
|
|
2044
|
+
});
|
|
2045
|
+
return {
|
|
2046
|
+
indexed,
|
|
2047
|
+
skipped,
|
|
2048
|
+
resolved: stats.resolvedCalls,
|
|
2049
|
+
unresolved: stats.unresolvedCalls,
|
|
2050
|
+
errors: this.errors
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
async discoverFiles(rootPath) {
|
|
2054
|
+
const allFiles = [];
|
|
2055
|
+
for (const pattern of this.config.include) {
|
|
2056
|
+
const matches = await glob(pattern, {
|
|
2057
|
+
cwd: rootPath,
|
|
2058
|
+
absolute: true,
|
|
2059
|
+
ignore: this.config.exclude,
|
|
2060
|
+
nodir: true
|
|
2061
|
+
});
|
|
2062
|
+
allFiles.push(...matches);
|
|
2063
|
+
}
|
|
2064
|
+
const supportedExts = /* @__PURE__ */ new Set([".py", ".ts", ".tsx", ".js", ".jsx"]);
|
|
2065
|
+
const uniqueFiles = [...new Set(allFiles)].filter((f) => {
|
|
2066
|
+
const ext = extname(f);
|
|
2067
|
+
if (!supportedExts.has(ext)) return false;
|
|
2068
|
+
try {
|
|
2069
|
+
const stats = statSync(f);
|
|
2070
|
+
if (stats.size > 2e5) return false;
|
|
2071
|
+
} catch {
|
|
2072
|
+
return false;
|
|
2073
|
+
}
|
|
2074
|
+
return true;
|
|
2075
|
+
});
|
|
2076
|
+
return uniqueFiles.sort();
|
|
2077
|
+
}
|
|
2078
|
+
hashContent(content) {
|
|
2079
|
+
return createHash("sha256").update(content).digest("hex").substring(0, 16);
|
|
2080
|
+
}
|
|
2081
|
+
async parseAndStore(filePath, hash, rootPath) {
|
|
2082
|
+
const language = this.getLanguage(filePath);
|
|
2083
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
2084
|
+
const parser = parserRegistry.getByLanguage(language);
|
|
2085
|
+
if (!parser) {
|
|
2086
|
+
throw new Error(`No parser found for language: ${language}`);
|
|
2087
|
+
}
|
|
2088
|
+
const analysis = parser.parse(filePath, content);
|
|
2089
|
+
this.storage.deleteFileData(filePath);
|
|
2090
|
+
const fileExports = {};
|
|
2091
|
+
for (const node of analysis.nodes) {
|
|
2092
|
+
const nodeHash = this.hashContent(`${node.name}:${node.startLine}:${node.endLine}`);
|
|
2093
|
+
const nodeWithHash = { ...node, hash: nodeHash };
|
|
2094
|
+
this.storage.insertNode(nodeWithHash);
|
|
2095
|
+
if (node.type === "function" || node.type === "class") {
|
|
2096
|
+
fileExports[node.name] = node.id;
|
|
2097
|
+
this.storage.registerExport(filePath, node.name, node.id);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
this.moduleMap.set(filePath, fileExports);
|
|
2101
|
+
for (const edge of analysis.edges) {
|
|
2102
|
+
this.storage.insertEdge(edge);
|
|
2103
|
+
}
|
|
2104
|
+
for (const imp of analysis.imports) {
|
|
2105
|
+
for (const spec of imp.specifiers) {
|
|
2106
|
+
this.storage.registerImport(filePath, spec, imp.source, imp.line);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
if (analysis.variableTypes) {
|
|
2110
|
+
for (const varType of analysis.variableTypes) {
|
|
2111
|
+
this.storage.registerVariableType(
|
|
2112
|
+
varType.variableName,
|
|
2113
|
+
varType.typeName,
|
|
2114
|
+
varType.scopeId,
|
|
2115
|
+
filePath,
|
|
2116
|
+
varType.line
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
const functionNodes = analysis.nodes.filter((n) => n.type === "function" || n.type === "method").map((n) => ({
|
|
2121
|
+
id: n.id,
|
|
2122
|
+
name: n.name,
|
|
2123
|
+
startLine: n.startLine,
|
|
2124
|
+
endLine: n.endLine
|
|
2125
|
+
}));
|
|
2126
|
+
if (functionNodes.length > 0) {
|
|
2127
|
+
analyzeFileForPhase3(filePath, functionNodes, this.storage.db);
|
|
2128
|
+
}
|
|
2129
|
+
if (analysis.databaseOperations) {
|
|
2130
|
+
for (const dbOp of analysis.databaseOperations) {
|
|
2131
|
+
this.storage.insertDatabaseOperation({
|
|
2132
|
+
id: `${dbOp.nodeId}:db:${dbOp.line}`,
|
|
2133
|
+
node_id: dbOp.nodeId,
|
|
2134
|
+
database_type: dbOp.databaseType,
|
|
2135
|
+
operation: dbOp.operation,
|
|
2136
|
+
collection: dbOp.collection,
|
|
2137
|
+
line: dbOp.line
|
|
2138
|
+
});
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
this.storage.upsertFile(filePath, language, hash);
|
|
2142
|
+
this.processFrameworkPatterns(analysis.nodes, filePath);
|
|
2143
|
+
}
|
|
2144
|
+
/**
|
|
2145
|
+
* Process framework-specific patterns (FastAPI routes, Depends, etc.)
|
|
2146
|
+
*/
|
|
2147
|
+
processFrameworkPatterns(nodes, filePath) {
|
|
2148
|
+
for (const node of nodes) {
|
|
2149
|
+
if (node.type === "function" && node.metadata) {
|
|
2150
|
+
const httpEndpoint = node.metadata.httpEndpoint;
|
|
2151
|
+
if (httpEndpoint) {
|
|
2152
|
+
const endpointId = `${httpEndpoint.method}:${httpEndpoint.path}`;
|
|
2153
|
+
this.storage.insertHttpEndpoint({
|
|
2154
|
+
id: endpointId,
|
|
2155
|
+
method: httpEndpoint.method,
|
|
2156
|
+
path: httpEndpoint.path,
|
|
2157
|
+
handler_node_id: node.id,
|
|
2158
|
+
container_id: void 0,
|
|
2159
|
+
// Will be set when docker-compose is parsed
|
|
2160
|
+
metadata: {
|
|
2161
|
+
filePath,
|
|
2162
|
+
functionName: node.name,
|
|
2163
|
+
line: node.startLine
|
|
2164
|
+
}
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
const depends = node.metadata.depends;
|
|
2168
|
+
if (depends) {
|
|
2169
|
+
for (const dep of depends) {
|
|
2170
|
+
const depId = `${node.id}:depends:${dep.parameterName}`;
|
|
2171
|
+
this.storage.insertFrameworkDependency({
|
|
2172
|
+
id: depId,
|
|
2173
|
+
source_node_id: node.id,
|
|
2174
|
+
target_node_id: void 0,
|
|
2175
|
+
// Will be resolved later
|
|
2176
|
+
framework: "fastapi",
|
|
2177
|
+
pattern: "depends",
|
|
2178
|
+
parameter_name: dep.parameterName,
|
|
2179
|
+
line: dep.line,
|
|
2180
|
+
unresolved_name: dep.dependencyName,
|
|
2181
|
+
metadata: {
|
|
2182
|
+
filePath,
|
|
2183
|
+
functionName: node.name
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
resolveAllCalls() {
|
|
2192
|
+
const unresolvedEdges = this.storage.db.prepare(`SELECT * FROM edges WHERE target_id LIKE 'ref:%'`).all();
|
|
2193
|
+
for (const edge of unresolvedEdges) {
|
|
2194
|
+
const refName = edge.target_id.replace("ref:", "");
|
|
2195
|
+
const sourceNode = this.storage.getNode(edge.source_id);
|
|
2196
|
+
if (!sourceNode) continue;
|
|
2197
|
+
const resolvedId = this.resolveReference(sourceNode.filePath, refName, edge.source_id);
|
|
2198
|
+
if (resolvedId) {
|
|
2199
|
+
this.storage.insertEdge({
|
|
2200
|
+
id: edge.id,
|
|
2201
|
+
type: edge.type,
|
|
2202
|
+
sourceId: edge.source_id,
|
|
2203
|
+
targetId: resolvedId,
|
|
2204
|
+
metadata: edge.metadata ? JSON.parse(edge.metadata) : void 0
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
this.resolveFrameworkDependencies();
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Resolve framework dependencies (e.g., FastAPI Depends())
|
|
2212
|
+
*/
|
|
2213
|
+
resolveFrameworkDependencies() {
|
|
2214
|
+
const unresolvedDeps = this.storage.getAllUnresolvedDependencies();
|
|
2215
|
+
for (const dep of unresolvedDeps) {
|
|
2216
|
+
const sourceNode = this.storage.getNode(dep.source_node_id);
|
|
2217
|
+
if (!sourceNode) continue;
|
|
2218
|
+
const resolvedId = this.resolveReference(sourceNode.filePath, dep.unresolved_name);
|
|
2219
|
+
if (resolvedId) {
|
|
2220
|
+
this.storage.updateFrameworkDependencyTarget(dep.id, resolvedId);
|
|
2221
|
+
this.storage.insertEdge({
|
|
2222
|
+
id: `${dep.id}:edge`,
|
|
2223
|
+
type: "calls",
|
|
2224
|
+
sourceId: dep.source_node_id,
|
|
2225
|
+
targetId: resolvedId,
|
|
2226
|
+
metadata: {
|
|
2227
|
+
framework: "fastapi",
|
|
2228
|
+
pattern: "depends",
|
|
2229
|
+
line: dep.line
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
resolveReference(sourceFilePath, refName, scopeNodeId) {
|
|
2236
|
+
if (refName.startsWith("super().") && scopeNodeId) {
|
|
2237
|
+
const methodName = refName.replace("super().", "");
|
|
2238
|
+
const resolvedSuper = this.storage.resolveSuperMethod(methodName, scopeNodeId);
|
|
2239
|
+
if (resolvedSuper) {
|
|
2240
|
+
return resolvedSuper;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
if (refName.includes(".")) {
|
|
2244
|
+
const parts = refName.split(".");
|
|
2245
|
+
if (parts.length === 2) {
|
|
2246
|
+
const [objectName, methodName] = parts;
|
|
2247
|
+
if (scopeNodeId) {
|
|
2248
|
+
const resolvedVarMethod = this.storage.resolveVariableMethod(objectName, methodName, scopeNodeId);
|
|
2249
|
+
if (resolvedVarMethod) {
|
|
2250
|
+
return resolvedVarMethod;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
const resolvedClassMethod = this.storage.resolveClassMethod(objectName, methodName, sourceFilePath);
|
|
2254
|
+
if (resolvedClassMethod) {
|
|
2255
|
+
return resolvedClassMethod;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
const nodesInFile = this.storage.getNodesByFile(sourceFilePath);
|
|
2260
|
+
for (const node of nodesInFile) {
|
|
2261
|
+
if (node.name === refName) {
|
|
2262
|
+
return node.id;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
const resolvedFromImport = this.storage.resolveImport(sourceFilePath, refName);
|
|
2266
|
+
if (resolvedFromImport) {
|
|
2267
|
+
return resolvedFromImport;
|
|
2268
|
+
}
|
|
2269
|
+
const matches = this.storage.searchNodesByName(refName);
|
|
2270
|
+
if (matches.length === 1) {
|
|
2271
|
+
return matches[0].id;
|
|
2272
|
+
}
|
|
2273
|
+
return null;
|
|
2274
|
+
}
|
|
2275
|
+
getLanguage(filePath) {
|
|
2276
|
+
const ext = extname(filePath);
|
|
2277
|
+
if (ext === ".py") return "python";
|
|
2278
|
+
if (ext === ".ts" || ext === ".tsx") return "typescript";
|
|
2279
|
+
if (ext === ".js" || ext === ".jsx") return "javascript";
|
|
2280
|
+
return "unknown";
|
|
2281
|
+
}
|
|
2282
|
+
emitProgress(progress) {
|
|
2283
|
+
if (this.onProgress) {
|
|
2284
|
+
this.onProgress(progress);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
|
|
2289
|
+
// src/types.ts
|
|
2290
|
+
var DEFAULT_CONFIG = {
|
|
2291
|
+
include: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "**/*.py"],
|
|
2292
|
+
exclude: [
|
|
2293
|
+
"**/node_modules/**",
|
|
2294
|
+
"**/dist/**",
|
|
2295
|
+
"**/build/**",
|
|
2296
|
+
"**/.git/**",
|
|
2297
|
+
"**/venv/**",
|
|
2298
|
+
"**/__pycache__/**",
|
|
2299
|
+
"**/.next/**"
|
|
2300
|
+
],
|
|
2301
|
+
languages: ["typescript", "javascript", "python"]
|
|
2302
|
+
};
|
|
2303
|
+
|
|
2304
|
+
// src/commands/impact.ts
|
|
2305
|
+
import { resolve as resolve2, join as join3, relative as relative2 } from "path";
|
|
2306
|
+
|
|
2307
|
+
// src/utils/git.ts
|
|
2308
|
+
import { execSync } from "child_process";
|
|
2309
|
+
import { existsSync } from "fs";
|
|
2310
|
+
import { join } from "path";
|
|
2311
|
+
function isGitRepository(path) {
|
|
2312
|
+
return existsSync(join(path, ".git"));
|
|
2313
|
+
}
|
|
2314
|
+
function getGitChangedFiles(rootPath) {
|
|
2315
|
+
try {
|
|
2316
|
+
const output = execSync("git diff HEAD --name-status", {
|
|
2317
|
+
cwd: rootPath,
|
|
2318
|
+
encoding: "utf-8"
|
|
2319
|
+
}).trim();
|
|
2320
|
+
if (!output) {
|
|
2321
|
+
return [];
|
|
2322
|
+
}
|
|
2323
|
+
const files = [];
|
|
2324
|
+
for (const line of output.split("\n")) {
|
|
2325
|
+
const [status, path] = line.split(" ");
|
|
2326
|
+
if (path) {
|
|
2327
|
+
files.push({
|
|
2328
|
+
path,
|
|
2329
|
+
status: status === "A" ? "added" : status === "D" ? "deleted" : "modified"
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
return files;
|
|
2334
|
+
} catch (error) {
|
|
2335
|
+
return [];
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
function parseGitDiffForFunctions(rootPath, files) {
|
|
2339
|
+
const changedFunctions = [];
|
|
2340
|
+
for (const file of files) {
|
|
2341
|
+
if (file.status === "deleted") {
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
try {
|
|
2345
|
+
const diff = execSync(`git diff HEAD -- "${file.path}"`, {
|
|
2346
|
+
cwd: rootPath,
|
|
2347
|
+
encoding: "utf-8"
|
|
2348
|
+
});
|
|
2349
|
+
const lines = diff.split("\n");
|
|
2350
|
+
let currentFunction = null;
|
|
2351
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2352
|
+
const line = lines[i];
|
|
2353
|
+
if (line.startsWith("@@")) {
|
|
2354
|
+
const contextMatch = line.match(/@@.*@@\s*(.*)/);
|
|
2355
|
+
if (contextMatch) {
|
|
2356
|
+
const context = contextMatch[1];
|
|
2357
|
+
const pythonMatch = context.match(/(?:async\s+)?def\s+(\w+)/);
|
|
2358
|
+
if (pythonMatch) {
|
|
2359
|
+
currentFunction = pythonMatch[1];
|
|
2360
|
+
}
|
|
2361
|
+
const jsMatch = context.match(/(?:function|const|class)\s+(\w+)/);
|
|
2362
|
+
if (jsMatch) {
|
|
2363
|
+
currentFunction = jsMatch[1];
|
|
2364
|
+
}
|
|
2365
|
+
const lineMatch = line.match(/\+(\d+)/);
|
|
2366
|
+
const lineNumber = lineMatch ? parseInt(lineMatch[1]) : 0;
|
|
2367
|
+
if (currentFunction) {
|
|
2368
|
+
changedFunctions.push({
|
|
2369
|
+
file: file.path,
|
|
2370
|
+
functionName: currentFunction,
|
|
2371
|
+
lineNumber,
|
|
2372
|
+
changeType: file.status === "added" ? "added" : "modified"
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
} catch (error) {
|
|
2379
|
+
continue;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return changedFunctions;
|
|
2383
|
+
}
|
|
2384
|
+
function getGitStatus(rootPath) {
|
|
2385
|
+
try {
|
|
2386
|
+
const statusOutput = execSync("git status --porcelain", {
|
|
2387
|
+
cwd: rootPath,
|
|
2388
|
+
encoding: "utf-8"
|
|
2389
|
+
}).trim();
|
|
2390
|
+
const lines = statusOutput.split("\n").filter((l) => l.trim());
|
|
2391
|
+
const staged = lines.filter((l) => l[0] !== " " && l[0] !== "?").length;
|
|
2392
|
+
const modified = lines.length;
|
|
2393
|
+
return {
|
|
2394
|
+
hasChanges: modified > 0,
|
|
2395
|
+
modifiedFiles: modified,
|
|
2396
|
+
stagedFiles: staged
|
|
2397
|
+
};
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
return {
|
|
2400
|
+
hasChanges: false,
|
|
2401
|
+
modifiedFiles: 0,
|
|
2402
|
+
stagedFiles: 0
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// src/analysis/imports.ts
|
|
2408
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2409
|
+
function isJavaScriptBuiltin(identifier) {
|
|
2410
|
+
const builtins = /* @__PURE__ */ new Set([
|
|
2411
|
+
"Object",
|
|
2412
|
+
"Array",
|
|
2413
|
+
"String",
|
|
2414
|
+
"Number",
|
|
2415
|
+
"Boolean",
|
|
2416
|
+
"Date",
|
|
2417
|
+
"RegExp",
|
|
2418
|
+
"Error",
|
|
2419
|
+
"TypeError",
|
|
2420
|
+
"SyntaxError",
|
|
2421
|
+
"ReferenceError",
|
|
2422
|
+
"Promise",
|
|
2423
|
+
"Map",
|
|
2424
|
+
"Set",
|
|
2425
|
+
"WeakMap",
|
|
2426
|
+
"WeakSet",
|
|
2427
|
+
"JSON",
|
|
2428
|
+
"Math",
|
|
2429
|
+
"Infinity",
|
|
2430
|
+
"NaN",
|
|
2431
|
+
"undefined",
|
|
2432
|
+
"Function",
|
|
2433
|
+
"Symbol",
|
|
2434
|
+
"Proxy",
|
|
2435
|
+
"Reflect",
|
|
2436
|
+
"Int8Array",
|
|
2437
|
+
"Uint8Array",
|
|
2438
|
+
"Int16Array",
|
|
2439
|
+
"Uint16Array",
|
|
2440
|
+
"Int32Array",
|
|
2441
|
+
"Uint32Array",
|
|
2442
|
+
"Float32Array",
|
|
2443
|
+
"Float64Array",
|
|
2444
|
+
"BigInt",
|
|
2445
|
+
"ArrayBuffer",
|
|
2446
|
+
"DataView",
|
|
2447
|
+
"Intl",
|
|
2448
|
+
"WebAssembly",
|
|
2449
|
+
"Buffer",
|
|
2450
|
+
"URL",
|
|
2451
|
+
"URLSearchParams",
|
|
2452
|
+
"console",
|
|
2453
|
+
"process",
|
|
2454
|
+
"global",
|
|
2455
|
+
"window",
|
|
2456
|
+
"document",
|
|
2457
|
+
// Common keywords that look like types
|
|
2458
|
+
"TRUE",
|
|
2459
|
+
"FALSE",
|
|
2460
|
+
"NULL"
|
|
2461
|
+
]);
|
|
2462
|
+
return builtins.has(identifier);
|
|
2463
|
+
}
|
|
2464
|
+
function isPythonBuiltin(identifier) {
|
|
2465
|
+
const builtins = /* @__PURE__ */ new Set([
|
|
2466
|
+
"True",
|
|
2467
|
+
"False",
|
|
2468
|
+
"None",
|
|
2469
|
+
"Exception",
|
|
2470
|
+
"BaseException",
|
|
2471
|
+
"ValueError",
|
|
2472
|
+
"TypeError",
|
|
2473
|
+
"KeyError",
|
|
2474
|
+
"AttributeError",
|
|
2475
|
+
"IndexError",
|
|
2476
|
+
"RuntimeError",
|
|
2477
|
+
"NotImplementedError",
|
|
2478
|
+
"Dict",
|
|
2479
|
+
"List",
|
|
2480
|
+
"Tuple",
|
|
2481
|
+
"Set",
|
|
2482
|
+
"FrozenSet",
|
|
2483
|
+
"Optional",
|
|
2484
|
+
"Union",
|
|
2485
|
+
"Any",
|
|
2486
|
+
"Callable"
|
|
2487
|
+
]);
|
|
2488
|
+
return builtins.has(identifier);
|
|
2489
|
+
}
|
|
2490
|
+
function analyzeJavaScriptImports(filePath) {
|
|
2491
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
2492
|
+
const lines = content.split("\n");
|
|
2493
|
+
const imports = /* @__PURE__ */ new Map();
|
|
2494
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2495
|
+
const line = lines[i];
|
|
2496
|
+
const defaultMatch = line.match(/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/);
|
|
2497
|
+
if (defaultMatch) {
|
|
2498
|
+
imports.set(defaultMatch[1], i + 1);
|
|
2499
|
+
}
|
|
2500
|
+
const namedMatch = line.match(/import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/);
|
|
2501
|
+
if (namedMatch) {
|
|
2502
|
+
const names = namedMatch[1].split(",").map((n) => n.trim().split(" as ")[0].trim());
|
|
2503
|
+
names.forEach((name) => imports.set(name, i + 1));
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
const usedIdentifiers = /* @__PURE__ */ new Map();
|
|
2507
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2508
|
+
const line = lines[i];
|
|
2509
|
+
if (line.trim().startsWith("import ") || line.trim().startsWith("//") || line.trim().startsWith("*")) {
|
|
2510
|
+
continue;
|
|
2511
|
+
}
|
|
2512
|
+
for (const [identifier] of imports) {
|
|
2513
|
+
const regex = new RegExp(`\\b${identifier}\\b`, "g");
|
|
2514
|
+
if (regex.test(line)) {
|
|
2515
|
+
if (!usedIdentifiers.has(identifier)) {
|
|
2516
|
+
usedIdentifiers.set(identifier, []);
|
|
2517
|
+
}
|
|
2518
|
+
usedIdentifiers.get(identifier).push(i + 1);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
const unused = [];
|
|
2523
|
+
for (const [identifier, line] of imports) {
|
|
2524
|
+
const usage = usedIdentifiers.get(identifier);
|
|
2525
|
+
if (!usage || usage.length === 0) {
|
|
2526
|
+
unused.push({ identifier, line, type: "unused" });
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
const missing = [];
|
|
2530
|
+
const localDefinitions = /* @__PURE__ */ new Set();
|
|
2531
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2532
|
+
const line = lines[i];
|
|
2533
|
+
const classMatch = line.match(/^(?:export\s+)?class\s+([A-Z][a-zA-Z0-9]*)/);
|
|
2534
|
+
if (classMatch) {
|
|
2535
|
+
localDefinitions.add(classMatch[1]);
|
|
2536
|
+
}
|
|
2537
|
+
const funcMatch = line.match(/^(?:export\s+)?(?:async\s+)?function\s+([A-Z][a-zA-Z0-9]*)/);
|
|
2538
|
+
if (funcMatch) {
|
|
2539
|
+
localDefinitions.add(funcMatch[1]);
|
|
2540
|
+
}
|
|
2541
|
+
const constMatch = line.match(/^(?:export\s+)?const\s+([A-Z][a-z][a-zA-Z0-9]*)\s*=/);
|
|
2542
|
+
if (constMatch) {
|
|
2543
|
+
localDefinitions.add(constMatch[1]);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
const potentialImports = /* @__PURE__ */ new Set();
|
|
2547
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2548
|
+
let line = lines[i];
|
|
2549
|
+
if (line.trim().startsWith("import ") || line.trim().startsWith("//") || line.trim().startsWith("*") || line.trim().startsWith("#")) {
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
line = line.replace(/\/\/.*$/g, "");
|
|
2553
|
+
line = line.replace(/\/\*.*?\*\//g, "");
|
|
2554
|
+
line = line.replace(/[`"'].*?[`"']/g, "");
|
|
2555
|
+
const matches = line.matchAll(/(?<!\.)([A-Z][a-z][a-zA-Z0-9]*)(?!\.)/g);
|
|
2556
|
+
for (const match of matches) {
|
|
2557
|
+
const identifier = match[1];
|
|
2558
|
+
if (!isJavaScriptBuiltin(identifier)) {
|
|
2559
|
+
potentialImports.add(identifier);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
for (const identifier of potentialImports) {
|
|
2564
|
+
if (!imports.has(identifier) && !localDefinitions.has(identifier)) {
|
|
2565
|
+
const usageLines = [];
|
|
2566
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2567
|
+
const line = lines[i];
|
|
2568
|
+
const regex = new RegExp(`\\b${identifier}\\b`);
|
|
2569
|
+
if (regex.test(line) && !line.trim().startsWith("//")) {
|
|
2570
|
+
usageLines.push(i + 1);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
if (usageLines.length > 0) {
|
|
2574
|
+
missing.push({
|
|
2575
|
+
identifier,
|
|
2576
|
+
line: usageLines[0],
|
|
2577
|
+
type: "missing",
|
|
2578
|
+
usedAt: usageLines
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
return { missing, unused };
|
|
2584
|
+
}
|
|
2585
|
+
function analyzePythonImports(filePath) {
|
|
2586
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
2587
|
+
const lines = content.split("\n");
|
|
2588
|
+
const imports = /* @__PURE__ */ new Map();
|
|
2589
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2590
|
+
const line = lines[i];
|
|
2591
|
+
const importMatch = line.match(/^import\s+(\w+)/);
|
|
2592
|
+
if (importMatch) {
|
|
2593
|
+
imports.set(importMatch[1], i + 1);
|
|
2594
|
+
}
|
|
2595
|
+
const fromMatch = line.match(/^from\s+[\w.]+\s+import\s+(.+)/);
|
|
2596
|
+
if (fromMatch) {
|
|
2597
|
+
const names = fromMatch[1].split(",").map((n) => n.trim().split(" as ")[0].trim());
|
|
2598
|
+
names.forEach((name) => imports.set(name, i + 1));
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
const usedIdentifiers = /* @__PURE__ */ new Map();
|
|
2602
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2603
|
+
const line = lines[i];
|
|
2604
|
+
if (line.trim().startsWith("import ") || line.trim().startsWith("from ") || line.trim().startsWith("#")) {
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
for (const [identifier] of imports) {
|
|
2608
|
+
const regex = new RegExp(`\\b${identifier}\\b`, "g");
|
|
2609
|
+
if (regex.test(line)) {
|
|
2610
|
+
if (!usedIdentifiers.has(identifier)) {
|
|
2611
|
+
usedIdentifiers.set(identifier, []);
|
|
2612
|
+
}
|
|
2613
|
+
usedIdentifiers.get(identifier).push(i + 1);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
const unused = [];
|
|
2618
|
+
for (const [identifier, line] of imports) {
|
|
2619
|
+
const usage = usedIdentifiers.get(identifier);
|
|
2620
|
+
if (!usage || usage.length === 0) {
|
|
2621
|
+
unused.push({ identifier, line, type: "unused" });
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
const missing = [];
|
|
2625
|
+
const localDefinitions = /* @__PURE__ */ new Set();
|
|
2626
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2627
|
+
const line = lines[i];
|
|
2628
|
+
const classMatch = line.match(/^class\s+([A-Z][a-zA-Z0-9_]*)/);
|
|
2629
|
+
if (classMatch) {
|
|
2630
|
+
localDefinitions.add(classMatch[1]);
|
|
2631
|
+
}
|
|
2632
|
+
const funcMatch = line.match(/^(?:async\s+)?def\s+([A-Z][a-zA-Z0-9_]*)/);
|
|
2633
|
+
if (funcMatch) {
|
|
2634
|
+
localDefinitions.add(funcMatch[1]);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
const potentialImports = /* @__PURE__ */ new Set();
|
|
2638
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2639
|
+
let line = lines[i];
|
|
2640
|
+
if (line.trim().startsWith("import ") || line.trim().startsWith("from ") || line.trim().startsWith("#")) {
|
|
2641
|
+
continue;
|
|
2642
|
+
}
|
|
2643
|
+
line = line.replace(/#.*$/g, "");
|
|
2644
|
+
line = line.replace(/[`"'f].*?[`"']/g, "");
|
|
2645
|
+
const matches = line.matchAll(/(?<!\.)([A-Z][a-zA-Z0-9_]*)(?!\.)/g);
|
|
2646
|
+
for (const match of matches) {
|
|
2647
|
+
const identifier = match[1];
|
|
2648
|
+
if (!isPythonBuiltin(identifier)) {
|
|
2649
|
+
potentialImports.add(identifier);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
for (const identifier of potentialImports) {
|
|
2654
|
+
if (!imports.has(identifier) && !localDefinitions.has(identifier)) {
|
|
2655
|
+
const usageLines = [];
|
|
2656
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2657
|
+
const line = lines[i];
|
|
2658
|
+
const regex = new RegExp(`\\b${identifier}\\b`);
|
|
2659
|
+
if (regex.test(line) && !line.trim().startsWith("#")) {
|
|
2660
|
+
usageLines.push(i + 1);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
if (usageLines.length > 0) {
|
|
2664
|
+
missing.push({
|
|
2665
|
+
identifier,
|
|
2666
|
+
line: usageLines[0],
|
|
2667
|
+
type: "missing",
|
|
2668
|
+
usedAt: usageLines
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
return { missing, unused };
|
|
2674
|
+
}
|
|
2675
|
+
function analyzeImports(filePath) {
|
|
2676
|
+
if (filePath.endsWith(".py")) {
|
|
2677
|
+
return analyzePythonImports(filePath);
|
|
2678
|
+
} else if (filePath.match(/\.(js|jsx|ts|tsx)$/)) {
|
|
2679
|
+
return analyzeJavaScriptImports(filePath);
|
|
2680
|
+
}
|
|
2681
|
+
return { missing: [], unused: [] };
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
// src/analysis/docker.ts
|
|
2685
|
+
import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
|
|
2686
|
+
import { load as parseYaml } from "js-yaml";
|
|
2687
|
+
import { join as join2 } from "path";
|
|
2688
|
+
function parseDockerfile(dockerfilePath) {
|
|
2689
|
+
if (!existsSync2(dockerfilePath)) {
|
|
2690
|
+
return null;
|
|
2691
|
+
}
|
|
2692
|
+
const content = readFileSync4(dockerfilePath, "utf-8");
|
|
2693
|
+
const lines = content.split("\n");
|
|
2694
|
+
let inHealthCheck = false;
|
|
2695
|
+
for (const line of lines) {
|
|
2696
|
+
const trimmed = line.trim();
|
|
2697
|
+
if (line.includes("HEALTHCHECK")) {
|
|
2698
|
+
inHealthCheck = true;
|
|
2699
|
+
continue;
|
|
2700
|
+
}
|
|
2701
|
+
if (inHealthCheck && !line.startsWith(" ") && !line.startsWith(" ")) {
|
|
2702
|
+
inHealthCheck = false;
|
|
2703
|
+
}
|
|
2704
|
+
if (inHealthCheck) {
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
2707
|
+
if (trimmed.startsWith("CMD")) {
|
|
2708
|
+
const match = trimmed.match(/CMD\s+\[([^\]]+)\]/);
|
|
2709
|
+
if (match) {
|
|
2710
|
+
const parts = match[1].split(",").map((p) => p.trim().replace(/"/g, ""));
|
|
2711
|
+
const commandStr = parts.join(" ");
|
|
2712
|
+
return extractEntryPointFromCommand(commandStr);
|
|
2713
|
+
}
|
|
2714
|
+
const shellMatch = trimmed.match(/CMD\s+(.+)/);
|
|
2715
|
+
if (shellMatch) {
|
|
2716
|
+
return extractEntryPointFromCommand(shellMatch[1]);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
if (trimmed.startsWith("ENTRYPOINT")) {
|
|
2720
|
+
const match = trimmed.match(/ENTRYPOINT\s+\[([^\]]+)\]/);
|
|
2721
|
+
if (match) {
|
|
2722
|
+
const parts = match[1].split(",").map((p) => p.trim().replace(/"/g, ""));
|
|
2723
|
+
const commandStr = parts.join(" ");
|
|
2724
|
+
return extractEntryPointFromCommand(commandStr);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
return null;
|
|
2729
|
+
}
|
|
2730
|
+
function extractEntryPointFromCommand(command) {
|
|
2731
|
+
const commandStr = Array.isArray(command) ? command.join(" ") : command;
|
|
2732
|
+
const patterns = [
|
|
2733
|
+
/node\s+([^\s]+\.js)/,
|
|
2734
|
+
/python\s+-m\s+([\w.]+)/,
|
|
2735
|
+
/python\s+([^\s]+\.py)/,
|
|
2736
|
+
/uvicorn\s+([\w:]+)/,
|
|
2737
|
+
/npm\s+start/
|
|
2738
|
+
];
|
|
2739
|
+
for (const pattern of patterns) {
|
|
2740
|
+
const match = commandStr.match(pattern);
|
|
2741
|
+
if (match) {
|
|
2742
|
+
const entryFile = match[1];
|
|
2743
|
+
if (entryFile.endsWith(".js") || entryFile.endsWith(".py")) {
|
|
2744
|
+
return entryFile;
|
|
2745
|
+
} else if (entryFile.includes(":")) {
|
|
2746
|
+
const file = entryFile.split(":")[0];
|
|
2747
|
+
return `${file}.py`;
|
|
2748
|
+
} else {
|
|
2749
|
+
const file = entryFile.replace(/\./g, "/");
|
|
2750
|
+
return `${file}.py`;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
return null;
|
|
2755
|
+
}
|
|
2756
|
+
function parseDockerCompose(projectRoot) {
|
|
2757
|
+
const graph = {
|
|
2758
|
+
services: /* @__PURE__ */ new Map(),
|
|
2759
|
+
entryPoints: /* @__PURE__ */ new Map()
|
|
2760
|
+
};
|
|
2761
|
+
const composeFiles = [
|
|
2762
|
+
join2(projectRoot, "docker-compose.yaml"),
|
|
2763
|
+
join2(projectRoot, "docker-compose.yml"),
|
|
2764
|
+
join2(projectRoot, "compose.yaml"),
|
|
2765
|
+
join2(projectRoot, "compose.yml")
|
|
2766
|
+
];
|
|
2767
|
+
let composeData = null;
|
|
2768
|
+
for (const file of composeFiles) {
|
|
2769
|
+
if (existsSync2(file)) {
|
|
2770
|
+
const content = readFileSync4(file, "utf-8");
|
|
2771
|
+
composeData = parseYaml(content);
|
|
2772
|
+
break;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
if (!composeData || !composeData.services) {
|
|
2776
|
+
return graph;
|
|
2777
|
+
}
|
|
2778
|
+
for (const [serviceName, serviceConfig] of Object.entries(composeData.services)) {
|
|
2779
|
+
const config = serviceConfig;
|
|
2780
|
+
const service = {
|
|
2781
|
+
name: serviceName,
|
|
2782
|
+
dependsOn: [],
|
|
2783
|
+
volumes: [],
|
|
2784
|
+
entryPoint: config.entrypoint,
|
|
2785
|
+
command: config.command
|
|
2786
|
+
};
|
|
2787
|
+
if (config.depends_on) {
|
|
2788
|
+
if (Array.isArray(config.depends_on)) {
|
|
2789
|
+
service.dependsOn = config.depends_on;
|
|
2790
|
+
} else if (typeof config.depends_on === "object") {
|
|
2791
|
+
service.dependsOn = Object.keys(config.depends_on);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
if (config.volumes && Array.isArray(config.volumes)) {
|
|
2795
|
+
service.volumes = config.volumes.filter((v) => typeof v === "string").map((v) => v.split(":")[0]);
|
|
2796
|
+
}
|
|
2797
|
+
if (config.command) {
|
|
2798
|
+
const entryFile = extractEntryPointFromCommand(config.command);
|
|
2799
|
+
if (entryFile) {
|
|
2800
|
+
graph.entryPoints.set(serviceName, entryFile);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
if (!graph.entryPoints.has(serviceName) && config.build) {
|
|
2804
|
+
const buildContext = typeof config.build === "string" ? config.build : config.build.context;
|
|
2805
|
+
if (buildContext) {
|
|
2806
|
+
const dockerfilePath = join2(projectRoot, buildContext, "Dockerfile");
|
|
2807
|
+
const entryFile = parseDockerfile(dockerfilePath);
|
|
2808
|
+
if (entryFile) {
|
|
2809
|
+
graph.entryPoints.set(serviceName, entryFile);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
graph.services.set(serviceName, service);
|
|
2814
|
+
}
|
|
2815
|
+
return graph;
|
|
2816
|
+
}
|
|
2817
|
+
function findDependentServices(graph, serviceName) {
|
|
2818
|
+
const dependents = [];
|
|
2819
|
+
for (const [name, service] of graph.services) {
|
|
2820
|
+
if (service.dependsOn.includes(serviceName)) {
|
|
2821
|
+
dependents.push(name);
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
return dependents;
|
|
2825
|
+
}
|
|
2826
|
+
function calculateServiceBlastRadius(graph, serviceName) {
|
|
2827
|
+
const affected = /* @__PURE__ */ new Set();
|
|
2828
|
+
const queue = [serviceName];
|
|
2829
|
+
while (queue.length > 0) {
|
|
2830
|
+
const current = queue.shift();
|
|
2831
|
+
if (affected.has(current)) continue;
|
|
2832
|
+
affected.add(current);
|
|
2833
|
+
const dependents = findDependentServices(graph, current);
|
|
2834
|
+
queue.push(...dependents);
|
|
2835
|
+
}
|
|
2836
|
+
return Array.from(affected);
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// src/analysis/line-diff.ts
|
|
2840
|
+
import { execSync as execSync2 } from "child_process";
|
|
2841
|
+
function parseLineLevelDiff(projectRoot) {
|
|
2842
|
+
try {
|
|
2843
|
+
const diffOutput = execSync2("git diff HEAD", {
|
|
2844
|
+
cwd: projectRoot,
|
|
2845
|
+
encoding: "utf-8",
|
|
2846
|
+
maxBuffer: 10 * 1024 * 1024
|
|
2847
|
+
// 10MB buffer
|
|
2848
|
+
});
|
|
2849
|
+
if (!diffOutput.trim()) {
|
|
2850
|
+
return {
|
|
2851
|
+
removedLines: [],
|
|
2852
|
+
addedLines: [],
|
|
2853
|
+
modifiedLines: []
|
|
2854
|
+
};
|
|
2855
|
+
}
|
|
2856
|
+
return parseDiffOutput(diffOutput);
|
|
2857
|
+
} catch (error) {
|
|
2858
|
+
return {
|
|
2859
|
+
removedLines: [],
|
|
2860
|
+
addedLines: [],
|
|
2861
|
+
modifiedLines: []
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
function parseDiffOutput(diffOutput) {
|
|
2866
|
+
const removedLines = [];
|
|
2867
|
+
const addedLines = [];
|
|
2868
|
+
let currentFile = "";
|
|
2869
|
+
let currentFunction = "";
|
|
2870
|
+
let removedLineNumber = 0;
|
|
2871
|
+
let addedLineNumber = 0;
|
|
2872
|
+
const lines = diffOutput.split("\n");
|
|
2873
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2874
|
+
const line = lines[i];
|
|
2875
|
+
if (line.startsWith("diff --git")) {
|
|
2876
|
+
const match = line.match(/b\/(.*?)$/);
|
|
2877
|
+
if (match) {
|
|
2878
|
+
currentFile = match[1].trim();
|
|
2879
|
+
}
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
if (line.startsWith("@@")) {
|
|
2883
|
+
const match = line.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@(.*)/);
|
|
2884
|
+
if (match) {
|
|
2885
|
+
removedLineNumber = parseInt(match[1]);
|
|
2886
|
+
addedLineNumber = parseInt(match[2]);
|
|
2887
|
+
const context = match[3].trim();
|
|
2888
|
+
if (context) {
|
|
2889
|
+
currentFunction = extractFunctionName(context);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("index ")) {
|
|
2895
|
+
continue;
|
|
2896
|
+
}
|
|
2897
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
2898
|
+
const content = line.substring(1);
|
|
2899
|
+
if (content.trim()) {
|
|
2900
|
+
removedLines.push({
|
|
2901
|
+
file: currentFile,
|
|
2902
|
+
lineNumber: removedLineNumber,
|
|
2903
|
+
content,
|
|
2904
|
+
inFunction: currentFunction || void 0
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
removedLineNumber++;
|
|
2908
|
+
continue;
|
|
2909
|
+
}
|
|
2910
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
2911
|
+
const content = line.substring(1);
|
|
2912
|
+
if (content.trim()) {
|
|
2913
|
+
addedLines.push({
|
|
2914
|
+
file: currentFile,
|
|
2915
|
+
lineNumber: addedLineNumber,
|
|
2916
|
+
content,
|
|
2917
|
+
inFunction: currentFunction || void 0
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
addedLineNumber++;
|
|
2921
|
+
continue;
|
|
2922
|
+
}
|
|
2923
|
+
if (line.startsWith(" ") || !line.startsWith("-") && !line.startsWith("+")) {
|
|
2924
|
+
removedLineNumber++;
|
|
2925
|
+
addedLineNumber++;
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
const modifiedLines = matchModifiedLines(removedLines, addedLines);
|
|
2929
|
+
return {
|
|
2930
|
+
removedLines,
|
|
2931
|
+
addedLines,
|
|
2932
|
+
modifiedLines
|
|
2933
|
+
};
|
|
2934
|
+
}
|
|
2935
|
+
function extractFunctionName(context) {
|
|
2936
|
+
context = context.replace(/^(function|def|class|const|let|var|async|export)\s+/, "");
|
|
2937
|
+
const patterns = [
|
|
2938
|
+
/^(\w+)\s*\(/,
|
|
2939
|
+
// functionName(
|
|
2940
|
+
/^(\w+)\s*=/,
|
|
2941
|
+
// functionName =
|
|
2942
|
+
/^(\w+)\s*{/,
|
|
2943
|
+
// ClassName {
|
|
2944
|
+
/^(\w+)\s*:/
|
|
2945
|
+
// functionName:
|
|
2946
|
+
];
|
|
2947
|
+
for (const pattern of patterns) {
|
|
2948
|
+
const match = context.match(pattern);
|
|
2949
|
+
if (match) {
|
|
2950
|
+
return match[1];
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
const firstWord = context.split(/\s+/)[0];
|
|
2954
|
+
return firstWord || "";
|
|
2955
|
+
}
|
|
2956
|
+
function matchModifiedLines(removedLines, addedLines) {
|
|
2957
|
+
const modifiedLines = [];
|
|
2958
|
+
const removedByLocation = /* @__PURE__ */ new Map();
|
|
2959
|
+
const addedByLocation = /* @__PURE__ */ new Map();
|
|
2960
|
+
for (const removed of removedLines) {
|
|
2961
|
+
const key = `${removed.file}:${removed.inFunction || "global"}`;
|
|
2962
|
+
if (!removedByLocation.has(key)) {
|
|
2963
|
+
removedByLocation.set(key, []);
|
|
2964
|
+
}
|
|
2965
|
+
removedByLocation.get(key).push(removed);
|
|
2966
|
+
}
|
|
2967
|
+
for (const added of addedLines) {
|
|
2968
|
+
const key = `${added.file}:${added.inFunction || "global"}`;
|
|
2969
|
+
if (!addedByLocation.has(key)) {
|
|
2970
|
+
addedByLocation.set(key, []);
|
|
2971
|
+
}
|
|
2972
|
+
addedByLocation.get(key).push(added);
|
|
2973
|
+
}
|
|
2974
|
+
for (const [location, removedLinesInLocation] of removedByLocation) {
|
|
2975
|
+
const addedLinesInLocation = addedByLocation.get(location) || [];
|
|
2976
|
+
for (const removed of removedLinesInLocation) {
|
|
2977
|
+
const matchingAdded = addedLinesInLocation.find(
|
|
2978
|
+
(added) => Math.abs(added.lineNumber - removed.lineNumber) <= 2
|
|
2979
|
+
);
|
|
2980
|
+
if (matchingAdded) {
|
|
2981
|
+
modifiedLines.push({
|
|
2982
|
+
before: removed,
|
|
2983
|
+
after: matchingAdded
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return modifiedLines;
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
// src/analysis/removed-calls.ts
|
|
2992
|
+
function extractRemovedCalls(removedLines) {
|
|
2993
|
+
const removedCalls = [];
|
|
2994
|
+
for (const line of removedLines) {
|
|
2995
|
+
const calls = parseLineForCalls(line.content, line.file);
|
|
2996
|
+
for (const call of calls) {
|
|
2997
|
+
removedCalls.push({
|
|
2998
|
+
callee: call.name,
|
|
2999
|
+
removedFrom: line.inFunction || "unknown",
|
|
3000
|
+
file: line.file,
|
|
3001
|
+
lineNumber: line.lineNumber,
|
|
3002
|
+
rawContent: line.content.trim()
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
return removedCalls;
|
|
3007
|
+
}
|
|
3008
|
+
function parseLineForCalls(lineContent, filePath) {
|
|
3009
|
+
const calls = [];
|
|
3010
|
+
if (isCommentOrString(lineContent)) {
|
|
3011
|
+
return calls;
|
|
3012
|
+
}
|
|
3013
|
+
lineContent = lineContent.replace(/\/\/.*$/g, "");
|
|
3014
|
+
lineContent = lineContent.replace(/#.*$/g, "");
|
|
3015
|
+
lineContent = lineContent.replace(/\/\*.*?\*\//g, "");
|
|
3016
|
+
lineContent = lineContent.replace(/["'`].*?["'`]/g, "");
|
|
3017
|
+
const isJS = /\.(js|ts|jsx|tsx)$/.test(filePath);
|
|
3018
|
+
const isPython = /\.py$/.test(filePath);
|
|
3019
|
+
if (isJS) {
|
|
3020
|
+
calls.push(...extractJavaScriptCalls(lineContent));
|
|
3021
|
+
}
|
|
3022
|
+
if (isPython) {
|
|
3023
|
+
calls.push(...extractPythonCalls(lineContent));
|
|
3024
|
+
}
|
|
3025
|
+
return calls;
|
|
3026
|
+
}
|
|
3027
|
+
function isCommentOrString(line) {
|
|
3028
|
+
const trimmed = line.trim();
|
|
3029
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("#")) {
|
|
3030
|
+
return true;
|
|
3031
|
+
}
|
|
3032
|
+
const stringPrefixes = ['"', "'", "`"];
|
|
3033
|
+
if (stringPrefixes.some((prefix) => trimmed.startsWith(prefix))) {
|
|
3034
|
+
return true;
|
|
3035
|
+
}
|
|
3036
|
+
return false;
|
|
3037
|
+
}
|
|
3038
|
+
function extractJavaScriptCalls(lineContent) {
|
|
3039
|
+
const calls = [];
|
|
3040
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3041
|
+
const simplePattern = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
3042
|
+
let match;
|
|
3043
|
+
while ((match = simplePattern.exec(lineContent)) !== null) {
|
|
3044
|
+
const functionName = match[1];
|
|
3045
|
+
if (!isJavaScriptKeyword(functionName) && !seen.has(functionName)) {
|
|
3046
|
+
calls.push({ name: functionName });
|
|
3047
|
+
seen.add(functionName);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
const methodPattern = /\.([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
3051
|
+
while ((match = methodPattern.exec(lineContent)) !== null) {
|
|
3052
|
+
const methodName = match[1];
|
|
3053
|
+
if (!isJavaScriptKeyword(methodName) && !seen.has(methodName)) {
|
|
3054
|
+
calls.push({ name: methodName });
|
|
3055
|
+
seen.add(methodName);
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
const awaitPattern = /await\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
3059
|
+
while ((match = awaitPattern.exec(lineContent)) !== null) {
|
|
3060
|
+
const functionName = match[1];
|
|
3061
|
+
if (!isJavaScriptKeyword(functionName) && !seen.has(functionName)) {
|
|
3062
|
+
calls.push({ name: functionName });
|
|
3063
|
+
seen.add(functionName);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
return calls;
|
|
3067
|
+
}
|
|
3068
|
+
function extractPythonCalls(lineContent) {
|
|
3069
|
+
const calls = [];
|
|
3070
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3071
|
+
const simplePattern = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
|
|
3072
|
+
let match;
|
|
3073
|
+
while ((match = simplePattern.exec(lineContent)) !== null) {
|
|
3074
|
+
const functionName = match[1];
|
|
3075
|
+
if (!isPythonKeyword(functionName) && !seen.has(functionName)) {
|
|
3076
|
+
calls.push({ name: functionName });
|
|
3077
|
+
seen.add(functionName);
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
const methodPattern = /\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
|
|
3081
|
+
while ((match = methodPattern.exec(lineContent)) !== null) {
|
|
3082
|
+
const methodName = match[1];
|
|
3083
|
+
if (!isPythonKeyword(methodName) && !seen.has(methodName)) {
|
|
3084
|
+
calls.push({ name: methodName });
|
|
3085
|
+
seen.add(methodName);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
const awaitPattern = /await\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
|
|
3089
|
+
while ((match = awaitPattern.exec(lineContent)) !== null) {
|
|
3090
|
+
const functionName = match[1];
|
|
3091
|
+
if (!isPythonKeyword(functionName) && !seen.has(functionName)) {
|
|
3092
|
+
calls.push({ name: functionName });
|
|
3093
|
+
seen.add(functionName);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
return calls;
|
|
3097
|
+
}
|
|
3098
|
+
function isJavaScriptKeyword(identifier) {
|
|
3099
|
+
const keywords = /* @__PURE__ */ new Set([
|
|
3100
|
+
// Keywords
|
|
3101
|
+
"if",
|
|
3102
|
+
"else",
|
|
3103
|
+
"for",
|
|
3104
|
+
"while",
|
|
3105
|
+
"do",
|
|
3106
|
+
"switch",
|
|
3107
|
+
"case",
|
|
3108
|
+
"break",
|
|
3109
|
+
"continue",
|
|
3110
|
+
"return",
|
|
3111
|
+
"throw",
|
|
3112
|
+
"try",
|
|
3113
|
+
"catch",
|
|
3114
|
+
"finally",
|
|
3115
|
+
"new",
|
|
3116
|
+
"typeof",
|
|
3117
|
+
"instanceof",
|
|
3118
|
+
"delete",
|
|
3119
|
+
"void",
|
|
3120
|
+
"async",
|
|
3121
|
+
"await",
|
|
3122
|
+
"yield",
|
|
3123
|
+
"import",
|
|
3124
|
+
"export",
|
|
3125
|
+
"from",
|
|
3126
|
+
"class",
|
|
3127
|
+
"extends",
|
|
3128
|
+
"super",
|
|
3129
|
+
"this",
|
|
3130
|
+
"static",
|
|
3131
|
+
"const",
|
|
3132
|
+
"let",
|
|
3133
|
+
"var",
|
|
3134
|
+
"function",
|
|
3135
|
+
"constructor",
|
|
3136
|
+
"get",
|
|
3137
|
+
"set",
|
|
3138
|
+
// Common builtins to skip
|
|
3139
|
+
"console",
|
|
3140
|
+
"parseInt",
|
|
3141
|
+
"parseFloat",
|
|
3142
|
+
"isNaN",
|
|
3143
|
+
"isFinite",
|
|
3144
|
+
"setTimeout",
|
|
3145
|
+
"setInterval",
|
|
3146
|
+
"clearTimeout",
|
|
3147
|
+
"clearInterval",
|
|
3148
|
+
"require",
|
|
3149
|
+
"module",
|
|
3150
|
+
"exports",
|
|
3151
|
+
"__dirname",
|
|
3152
|
+
"__filename"
|
|
3153
|
+
]);
|
|
3154
|
+
return keywords.has(identifier);
|
|
3155
|
+
}
|
|
3156
|
+
function isPythonKeyword(identifier) {
|
|
3157
|
+
const keywords = /* @__PURE__ */ new Set([
|
|
3158
|
+
// Keywords
|
|
3159
|
+
"if",
|
|
3160
|
+
"elif",
|
|
3161
|
+
"else",
|
|
3162
|
+
"for",
|
|
3163
|
+
"while",
|
|
3164
|
+
"break",
|
|
3165
|
+
"continue",
|
|
3166
|
+
"pass",
|
|
3167
|
+
"return",
|
|
3168
|
+
"raise",
|
|
3169
|
+
"try",
|
|
3170
|
+
"except",
|
|
3171
|
+
"finally",
|
|
3172
|
+
"with",
|
|
3173
|
+
"as",
|
|
3174
|
+
"yield",
|
|
3175
|
+
"import",
|
|
3176
|
+
"from",
|
|
3177
|
+
"class",
|
|
3178
|
+
"def",
|
|
3179
|
+
"lambda",
|
|
3180
|
+
"global",
|
|
3181
|
+
"nonlocal",
|
|
3182
|
+
"assert",
|
|
3183
|
+
"del",
|
|
3184
|
+
"in",
|
|
3185
|
+
"is",
|
|
3186
|
+
"not",
|
|
3187
|
+
"and",
|
|
3188
|
+
"or",
|
|
3189
|
+
"async",
|
|
3190
|
+
"await",
|
|
3191
|
+
// Common builtins to skip
|
|
3192
|
+
"print",
|
|
3193
|
+
"len",
|
|
3194
|
+
"range",
|
|
3195
|
+
"enumerate",
|
|
3196
|
+
"zip",
|
|
3197
|
+
"map",
|
|
3198
|
+
"filter",
|
|
3199
|
+
"sorted",
|
|
3200
|
+
"reversed",
|
|
3201
|
+
"sum",
|
|
3202
|
+
"min",
|
|
3203
|
+
"max",
|
|
3204
|
+
"abs",
|
|
3205
|
+
"round",
|
|
3206
|
+
"open",
|
|
3207
|
+
"input",
|
|
3208
|
+
"type",
|
|
3209
|
+
"isinstance",
|
|
3210
|
+
"hasattr",
|
|
3211
|
+
"getattr",
|
|
3212
|
+
"setattr"
|
|
3213
|
+
]);
|
|
3214
|
+
return keywords.has(identifier);
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
// src/analysis/removed-call-impact.ts
|
|
3218
|
+
var SECURITY_KEYWORDS = [
|
|
3219
|
+
// Authentication & Authorization
|
|
3220
|
+
"authenticate",
|
|
3221
|
+
"authorization",
|
|
3222
|
+
"authorize",
|
|
3223
|
+
"login",
|
|
3224
|
+
"logout",
|
|
3225
|
+
"signin",
|
|
3226
|
+
"signout",
|
|
3227
|
+
"verify",
|
|
3228
|
+
"check",
|
|
3229
|
+
"validate",
|
|
3230
|
+
"permission",
|
|
3231
|
+
"access",
|
|
3232
|
+
"role",
|
|
3233
|
+
"admin",
|
|
3234
|
+
"auth",
|
|
3235
|
+
// Input validation & sanitization
|
|
3236
|
+
"sanitize",
|
|
3237
|
+
"escape",
|
|
3238
|
+
"clean",
|
|
3239
|
+
"filter",
|
|
3240
|
+
"whitelist",
|
|
3241
|
+
"blacklist",
|
|
3242
|
+
"validate",
|
|
3243
|
+
"verification",
|
|
3244
|
+
"check",
|
|
3245
|
+
// Security tokens & sessions
|
|
3246
|
+
"token",
|
|
3247
|
+
"session",
|
|
3248
|
+
"cookie",
|
|
3249
|
+
"csrf",
|
|
3250
|
+
"xsrf",
|
|
3251
|
+
"jwt",
|
|
3252
|
+
"oauth",
|
|
3253
|
+
"api_key",
|
|
3254
|
+
"apikey",
|
|
3255
|
+
"secret",
|
|
3256
|
+
"password",
|
|
3257
|
+
"hash",
|
|
3258
|
+
"encrypt",
|
|
3259
|
+
// Injection prevention
|
|
3260
|
+
"xss",
|
|
3261
|
+
"sql",
|
|
3262
|
+
"injection",
|
|
3263
|
+
"query",
|
|
3264
|
+
"prepared",
|
|
3265
|
+
"statement",
|
|
3266
|
+
// Rate limiting & abuse prevention
|
|
3267
|
+
"ratelimit",
|
|
3268
|
+
"throttle",
|
|
3269
|
+
"captcha",
|
|
3270
|
+
"honeypot"
|
|
3271
|
+
];
|
|
3272
|
+
function analyzeRemovedCallImpact(removedCalls, storage) {
|
|
3273
|
+
const impacts = [];
|
|
3274
|
+
for (const removedCall of removedCalls) {
|
|
3275
|
+
try {
|
|
3276
|
+
const impact = analyzeSingleRemovedCall(removedCall, storage);
|
|
3277
|
+
if (impact) {
|
|
3278
|
+
impacts.push(impact);
|
|
3279
|
+
}
|
|
3280
|
+
} catch (error) {
|
|
3281
|
+
console.error(`Error analyzing removed call ${removedCall.callee}:`, error);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
return impacts;
|
|
3285
|
+
}
|
|
3286
|
+
function analyzeSingleRemovedCall(removedCall, storage) {
|
|
3287
|
+
const nodes = storage.searchNodesByName(removedCall.callee);
|
|
3288
|
+
if (nodes.length === 0) {
|
|
3289
|
+
return null;
|
|
3290
|
+
}
|
|
3291
|
+
const functionNode = nodes[0];
|
|
3292
|
+
const allCallers = storage.getResolvedCallers(functionNode.id);
|
|
3293
|
+
const stillCalledBy = allCallers.filter((c) => c.name !== removedCall.removedFrom).map((c) => ({
|
|
3294
|
+
name: c.name,
|
|
3295
|
+
file: c.filePath,
|
|
3296
|
+
line: c.startLine
|
|
3297
|
+
}));
|
|
3298
|
+
const removedFromNodes = storage.searchNodesByName(removedCall.removedFrom);
|
|
3299
|
+
let impactedFunctions = [];
|
|
3300
|
+
if (removedFromNodes.length > 0) {
|
|
3301
|
+
const removedFromNode = removedFromNodes[0];
|
|
3302
|
+
const impactedNodes = storage.getImpactedNodes(removedFromNode.id, 5);
|
|
3303
|
+
impactedFunctions = impactedNodes.map((n) => ({
|
|
3304
|
+
name: n.name,
|
|
3305
|
+
file: n.filePath,
|
|
3306
|
+
line: n.startLine,
|
|
3307
|
+
reason: `Calls ${removedCall.removedFrom}() which no longer calls ${removedCall.callee}()`
|
|
3308
|
+
}));
|
|
3309
|
+
}
|
|
3310
|
+
const severity = calculateSeverity(
|
|
3311
|
+
removedCall,
|
|
3312
|
+
stillCalledBy,
|
|
3313
|
+
impactedFunctions
|
|
3314
|
+
);
|
|
3315
|
+
const description = buildImpactDescription(
|
|
3316
|
+
removedCall,
|
|
3317
|
+
stillCalledBy,
|
|
3318
|
+
impactedFunctions,
|
|
3319
|
+
severity
|
|
3320
|
+
);
|
|
3321
|
+
return {
|
|
3322
|
+
removedCall,
|
|
3323
|
+
functionNode: {
|
|
3324
|
+
id: functionNode.id,
|
|
3325
|
+
name: functionNode.name,
|
|
3326
|
+
file: functionNode.filePath,
|
|
3327
|
+
line: functionNode.startLine
|
|
3328
|
+
},
|
|
3329
|
+
stillCalledBy,
|
|
3330
|
+
impactedFunctions,
|
|
3331
|
+
severity,
|
|
3332
|
+
description
|
|
3333
|
+
};
|
|
3334
|
+
}
|
|
3335
|
+
function calculateSeverity(removedCall, stillCalledBy, impactedFunctions) {
|
|
3336
|
+
const isSecurityFunction = SECURITY_KEYWORDS.some(
|
|
3337
|
+
(keyword) => removedCall.callee.toLowerCase().includes(keyword)
|
|
3338
|
+
);
|
|
3339
|
+
if (isSecurityFunction && impactedFunctions.length > 5) {
|
|
3340
|
+
return "CRITICAL";
|
|
3341
|
+
}
|
|
3342
|
+
if (isSecurityFunction && stillCalledBy.length === 0) {
|
|
3343
|
+
return "CRITICAL";
|
|
3344
|
+
}
|
|
3345
|
+
if (stillCalledBy.length === 0) {
|
|
3346
|
+
return "HIGH";
|
|
3347
|
+
}
|
|
3348
|
+
if (impactedFunctions.length > 20) {
|
|
3349
|
+
return "HIGH";
|
|
3350
|
+
}
|
|
3351
|
+
if (impactedFunctions.length >= 10) {
|
|
3352
|
+
return "MEDIUM";
|
|
3353
|
+
}
|
|
3354
|
+
if (isSecurityFunction && impactedFunctions.length > 0) {
|
|
3355
|
+
return "MEDIUM";
|
|
3356
|
+
}
|
|
3357
|
+
return "LOW";
|
|
3358
|
+
}
|
|
3359
|
+
function buildImpactDescription(removedCall, stillCalledBy, impactedFunctions, severity) {
|
|
3360
|
+
const parts = [];
|
|
3361
|
+
parts.push(
|
|
3362
|
+
`${removedCall.callee}() was removed from ${removedCall.removedFrom}() at line ${removedCall.lineNumber}`
|
|
3363
|
+
);
|
|
3364
|
+
if (stillCalledBy.length === 0) {
|
|
3365
|
+
parts.push(`\u26A0\uFE0F ORPHANED: ${removedCall.callee}() is no longer called anywhere in the codebase`);
|
|
3366
|
+
} else {
|
|
3367
|
+
const callerNames = stillCalledBy.slice(0, 3).map((c) => c.name).join(", ");
|
|
3368
|
+
const moreCount = stillCalledBy.length > 3 ? ` and ${stillCalledBy.length - 3} more` : "";
|
|
3369
|
+
parts.push(`Still called by: ${callerNames}${moreCount}`);
|
|
3370
|
+
}
|
|
3371
|
+
if (impactedFunctions.length > 0) {
|
|
3372
|
+
parts.push(
|
|
3373
|
+
`Impact: ${impactedFunctions.length} function${impactedFunctions.length === 1 ? "" : "s"} depend on ${removedCall.removedFrom}()`
|
|
3374
|
+
);
|
|
3375
|
+
}
|
|
3376
|
+
if (severity === "CRITICAL") {
|
|
3377
|
+
parts.push("\u{1F534} CRITICAL: Security or validation function removed from execution path");
|
|
3378
|
+
}
|
|
3379
|
+
return parts.join("\n");
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
// src/commands/impact.ts
|
|
3383
|
+
async function analyzeImpact(options = {}) {
|
|
3384
|
+
const rootPath = resolve2(options.path || ".");
|
|
3385
|
+
const dbPath = join3(rootPath, ".codemap", "graph.db");
|
|
3386
|
+
const storage = new FlowStorage(dbPath);
|
|
3387
|
+
try {
|
|
3388
|
+
if (isGitRepository(rootPath)) {
|
|
3389
|
+
return await analyzeImpactWithGit(storage, rootPath, options);
|
|
3390
|
+
} else {
|
|
3391
|
+
return await analyzeImpactWithHash(storage, rootPath, options);
|
|
3392
|
+
}
|
|
3393
|
+
} finally {
|
|
3394
|
+
storage.close();
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
async function analyzeImpactWithGit(storage, rootPath, options) {
|
|
3398
|
+
const gitStatus = getGitStatus(rootPath);
|
|
3399
|
+
if (!gitStatus.hasChanges) {
|
|
3400
|
+
return {
|
|
3401
|
+
changedFiles: [],
|
|
3402
|
+
changedFunctions: [],
|
|
3403
|
+
directCallers: /* @__PURE__ */ new Map(),
|
|
3404
|
+
affectedEndpoints: [],
|
|
3405
|
+
importIssues: /* @__PURE__ */ new Map(),
|
|
3406
|
+
removedCallImpacts: [],
|
|
3407
|
+
// NEW
|
|
3408
|
+
riskLevel: "LOW",
|
|
3409
|
+
totalImpact: 0
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
const changedFiles = getGitChangedFiles(rootPath);
|
|
3413
|
+
const changedFilePaths = changedFiles.map((f) => f.path);
|
|
3414
|
+
const changedFunctions = parseGitDiffForFunctions(rootPath, changedFiles);
|
|
3415
|
+
const builder = new FlowBuilder(storage, {
|
|
3416
|
+
rootPath,
|
|
3417
|
+
include: changedFilePaths.map((f) => f),
|
|
3418
|
+
exclude: [],
|
|
3419
|
+
forceReindex: true
|
|
3420
|
+
});
|
|
3421
|
+
await builder.build();
|
|
3422
|
+
const directCallers = /* @__PURE__ */ new Map();
|
|
3423
|
+
const affectedEndpoints = [];
|
|
3424
|
+
for (const func of changedFunctions) {
|
|
3425
|
+
const nodes = storage.getNodesByFile(join3(rootPath, func.file)).filter((n) => n.name === func.functionName);
|
|
3426
|
+
for (const node of nodes) {
|
|
3427
|
+
const callers = storage.getResolvedCallersWithLocation(node.id);
|
|
3428
|
+
if (callers.length > 0) {
|
|
3429
|
+
directCallers.set(func.functionName, callers);
|
|
3430
|
+
}
|
|
3431
|
+
const endpoints = storage.getHttpEndpointsByHandler(node.id);
|
|
3432
|
+
for (const endpoint of endpoints) {
|
|
3433
|
+
affectedEndpoints.push(`${endpoint.method} ${endpoint.path}`);
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
const importIssues = /* @__PURE__ */ new Map();
|
|
3438
|
+
for (const filePath of changedFilePaths) {
|
|
3439
|
+
const fullPath = join3(rootPath, filePath);
|
|
3440
|
+
const analysis = analyzeImports(fullPath);
|
|
3441
|
+
if (analysis.missing.length > 0 || analysis.unused.length > 0) {
|
|
3442
|
+
importIssues.set(relative2(rootPath, fullPath), analysis);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
const dockerGraph = parseDockerCompose(rootPath);
|
|
3446
|
+
let dockerImpact;
|
|
3447
|
+
const entryPointFiles = [];
|
|
3448
|
+
const affectedServices = /* @__PURE__ */ new Set();
|
|
3449
|
+
for (const [serviceName, entryPoint] of dockerGraph.entryPoints) {
|
|
3450
|
+
for (const changedFile of changedFilePaths) {
|
|
3451
|
+
if (changedFile.includes(entryPoint) || entryPoint.includes(changedFile)) {
|
|
3452
|
+
entryPointFiles.push(entryPoint);
|
|
3453
|
+
const blastRadius = calculateServiceBlastRadius(dockerGraph, serviceName);
|
|
3454
|
+
blastRadius.forEach((s) => affectedServices.add(s));
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
if (affectedServices.size > 0) {
|
|
3459
|
+
dockerImpact = {
|
|
3460
|
+
affectedServices: Array.from(affectedServices),
|
|
3461
|
+
entryPointFiles
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
const lineDiff = parseLineLevelDiff(rootPath);
|
|
3465
|
+
const removedCalls = extractRemovedCalls(lineDiff.removedLines);
|
|
3466
|
+
const removedCallImpacts = analyzeRemovedCallImpact(removedCalls, storage);
|
|
3467
|
+
const totalCallers = Array.from(directCallers.values()).reduce((sum, callers) => sum + callers.length, 0);
|
|
3468
|
+
const hasCriticalImportIssues = Array.from(importIssues.values()).some((i) => i.missing.length > 0);
|
|
3469
|
+
const hasDockerImpact = dockerImpact && dockerImpact.affectedServices.length > 0;
|
|
3470
|
+
const hasCriticalRemovedCalls = removedCallImpacts.some((i) => i.severity === "CRITICAL");
|
|
3471
|
+
const hasHighRiskRemovedCalls = removedCallImpacts.some((i) => i.severity === "HIGH");
|
|
3472
|
+
let riskLevel = "LOW";
|
|
3473
|
+
if (hasCriticalRemovedCalls || hasCriticalImportIssues && hasDockerImpact) {
|
|
3474
|
+
riskLevel = "CRITICAL";
|
|
3475
|
+
} else if (hasHighRiskRemovedCalls || hasCriticalImportIssues || hasDockerImpact && dockerImpact.affectedServices.length > 2) {
|
|
3476
|
+
riskLevel = "HIGH";
|
|
3477
|
+
} else if (totalCallers > 20 || affectedEndpoints.length > 5) {
|
|
3478
|
+
riskLevel = "HIGH";
|
|
3479
|
+
} else if (totalCallers > 10 || affectedEndpoints.length > 2 || hasDockerImpact || removedCallImpacts.length > 0) {
|
|
3480
|
+
riskLevel = "MEDIUM";
|
|
3481
|
+
}
|
|
3482
|
+
return {
|
|
3483
|
+
changedFiles: changedFilePaths,
|
|
3484
|
+
changedFunctions,
|
|
3485
|
+
directCallers,
|
|
3486
|
+
affectedEndpoints,
|
|
3487
|
+
importIssues,
|
|
3488
|
+
dockerImpact,
|
|
3489
|
+
removedCallImpacts,
|
|
3490
|
+
// NEW
|
|
3491
|
+
riskLevel,
|
|
3492
|
+
totalImpact: totalCallers + (dockerImpact?.affectedServices.length || 0) + removedCallImpacts.length
|
|
3493
|
+
};
|
|
3494
|
+
}
|
|
3495
|
+
async function analyzeImpactWithHash(storage, rootPath, options) {
|
|
3496
|
+
const oldHashes = storage.getAllFileHashes();
|
|
3497
|
+
const builder = new FlowBuilder(storage, {
|
|
3498
|
+
rootPath,
|
|
3499
|
+
include: DEFAULT_CONFIG.include,
|
|
3500
|
+
exclude: DEFAULT_CONFIG.exclude,
|
|
3501
|
+
forceReindex: false
|
|
3502
|
+
// Use hash comparison
|
|
3503
|
+
});
|
|
3504
|
+
const result = await builder.build();
|
|
3505
|
+
const changedFiles = [];
|
|
3506
|
+
const newHashes = storage.getAllFileHashes();
|
|
3507
|
+
for (const [filePath, newHash] of Object.entries(newHashes)) {
|
|
3508
|
+
const oldHash = oldHashes[filePath];
|
|
3509
|
+
if (!oldHash || oldHash !== newHash) {
|
|
3510
|
+
changedFiles.push(filePath);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
const importIssues = /* @__PURE__ */ new Map();
|
|
3514
|
+
for (const filePath of changedFiles) {
|
|
3515
|
+
const analysis = analyzeImports(filePath);
|
|
3516
|
+
if (analysis.missing.length > 0 || analysis.unused.length > 0) {
|
|
3517
|
+
importIssues.set(relative2(rootPath, filePath), analysis);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
const hasCriticalImportIssues = Array.from(importIssues.values()).some((i) => i.missing.length > 0);
|
|
3521
|
+
return {
|
|
3522
|
+
changedFiles,
|
|
3523
|
+
changedFunctions: [],
|
|
3524
|
+
directCallers: /* @__PURE__ */ new Map(),
|
|
3525
|
+
affectedEndpoints: [],
|
|
3526
|
+
importIssues,
|
|
3527
|
+
removedCallImpacts: [],
|
|
3528
|
+
// NEW - not available in hash mode
|
|
3529
|
+
riskLevel: hasCriticalImportIssues ? "CRITICAL" : changedFiles.length > 10 ? "HIGH" : changedFiles.length > 5 ? "MEDIUM" : "LOW",
|
|
3530
|
+
totalImpact: result.indexed
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
export {
|
|
3535
|
+
FlowBuilder,
|
|
3536
|
+
DEFAULT_CONFIG,
|
|
3537
|
+
analyzeImpact
|
|
3538
|
+
};
|
|
3539
|
+
//# sourceMappingURL=chunk-6TAWVGMT.js.map
|