codemap-ai 3.2.0 → 3.5.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 +28 -1
- 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 +1828 -44
- package/dist/cli.js.map +1 -1
- package/dist/{flow-server-UQEOUP2C.js → flow-server-FMPWJSLK.js} +3 -3
- package/dist/{flow-server-UQEOUP2C.js.map → flow-server-FMPWJSLK.js.map} +1 -1
- package/dist/index.js +1052 -21
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +2 -2
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-LXZ73T7X.js.map +0 -1
- package/dist/chunk-VB74K47A.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -2,22 +2,408 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
FlowStorage
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-BRVRY5KT.js";
|
|
6
6
|
|
|
7
7
|
// src/cli-flow.ts
|
|
8
8
|
import { Command } from "commander";
|
|
9
9
|
import { resolve as resolve3, join as join4 } from "path";
|
|
10
|
-
import { existsSync as existsSync3, mkdirSync, readFileSync as
|
|
10
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync5, writeFileSync } from "fs";
|
|
11
11
|
import { homedir } from "os";
|
|
12
12
|
import chalk from "chalk";
|
|
13
13
|
import ora from "ora";
|
|
14
14
|
|
|
15
15
|
// src/flow/builder.ts
|
|
16
|
-
import { readFileSync, statSync } from "fs";
|
|
16
|
+
import { readFileSync as readFileSync2, statSync } from "fs";
|
|
17
17
|
import { createHash } from "crypto";
|
|
18
18
|
import { glob } from "glob";
|
|
19
19
|
import { resolve, relative, extname } from "path";
|
|
20
20
|
|
|
21
|
+
// src/analysis/class-hierarchy.ts
|
|
22
|
+
function buildClassHierarchy(nodes) {
|
|
23
|
+
const classes = /* @__PURE__ */ new Map();
|
|
24
|
+
const children = /* @__PURE__ */ new Map();
|
|
25
|
+
const parents = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const node of nodes) {
|
|
27
|
+
if (node.type === "class") {
|
|
28
|
+
const classNode = {
|
|
29
|
+
id: node.id,
|
|
30
|
+
name: node.name,
|
|
31
|
+
file: node.filePath,
|
|
32
|
+
line: node.startLine,
|
|
33
|
+
extends: node.extends || [],
|
|
34
|
+
methods: []
|
|
35
|
+
// Will populate in next step
|
|
36
|
+
};
|
|
37
|
+
classes.set(node.name, classNode);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const node of nodes) {
|
|
41
|
+
if (node.type === "method" && node.parentClass) {
|
|
42
|
+
const classNode = classes.get(node.parentClass);
|
|
43
|
+
if (classNode) {
|
|
44
|
+
classNode.methods.push(node.name);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const [className, classNode] of classes) {
|
|
49
|
+
for (const parentName of classNode.extends) {
|
|
50
|
+
if (!children.has(parentName)) {
|
|
51
|
+
children.set(parentName, []);
|
|
52
|
+
}
|
|
53
|
+
children.get(parentName).push(className);
|
|
54
|
+
if (!parents.has(className)) {
|
|
55
|
+
parents.set(className, []);
|
|
56
|
+
}
|
|
57
|
+
parents.get(className).push(parentName);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
classes,
|
|
62
|
+
children,
|
|
63
|
+
parents
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function findMethodDefinition(className, methodName, hierarchy) {
|
|
67
|
+
const visited = /* @__PURE__ */ new Set();
|
|
68
|
+
const queue = [className];
|
|
69
|
+
while (queue.length > 0) {
|
|
70
|
+
const current = queue.shift();
|
|
71
|
+
if (visited.has(current)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
visited.add(current);
|
|
75
|
+
const classNode = hierarchy.classes.get(current);
|
|
76
|
+
if (classNode) {
|
|
77
|
+
if (classNode.methods.includes(methodName)) {
|
|
78
|
+
return classNode;
|
|
79
|
+
}
|
|
80
|
+
const parentNames = hierarchy.parents.get(current) || [];
|
|
81
|
+
for (const parent of parentNames) {
|
|
82
|
+
queue.push(parent);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
function resolveMethodCall(instanceType, methodName, hierarchy) {
|
|
89
|
+
return findMethodDefinition(instanceType, methodName, hierarchy);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/flow/enhanced-resolver.ts
|
|
93
|
+
var EnhancedResolver = class {
|
|
94
|
+
constructor(storage) {
|
|
95
|
+
this.storage = storage;
|
|
96
|
+
}
|
|
97
|
+
hierarchy = null;
|
|
98
|
+
variableTypes = /* @__PURE__ */ new Map();
|
|
99
|
+
/**
|
|
100
|
+
* Build class hierarchy from all nodes in storage
|
|
101
|
+
*/
|
|
102
|
+
buildHierarchy() {
|
|
103
|
+
const nodes = this.getAllNodes();
|
|
104
|
+
this.hierarchy = buildClassHierarchy(nodes);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Load variable type information from storage
|
|
108
|
+
*/
|
|
109
|
+
loadVariableTypes() {
|
|
110
|
+
const stmt = this.storage.db.prepare(`
|
|
111
|
+
SELECT * FROM variable_types
|
|
112
|
+
`);
|
|
113
|
+
const rows = stmt.all();
|
|
114
|
+
for (const row of rows) {
|
|
115
|
+
const scopeId = row.scope_node_id;
|
|
116
|
+
if (!this.variableTypes.has(scopeId)) {
|
|
117
|
+
this.variableTypes.set(scopeId, []);
|
|
118
|
+
}
|
|
119
|
+
this.variableTypes.get(scopeId).push({
|
|
120
|
+
name: row.variable_name,
|
|
121
|
+
type: row.type_name,
|
|
122
|
+
scope: scopeId,
|
|
123
|
+
line: row.line
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Resolve all unresolved edges using type information
|
|
129
|
+
*/
|
|
130
|
+
resolveAllCalls() {
|
|
131
|
+
if (!this.hierarchy) {
|
|
132
|
+
this.buildHierarchy();
|
|
133
|
+
}
|
|
134
|
+
this.loadVariableTypes();
|
|
135
|
+
const resolvedCalls = [];
|
|
136
|
+
const stmt = this.storage.db.prepare(`
|
|
137
|
+
SELECT * FROM edges
|
|
138
|
+
WHERE target_id LIKE 'ref:%'
|
|
139
|
+
AND type = 'calls'
|
|
140
|
+
`);
|
|
141
|
+
const edges = stmt.all();
|
|
142
|
+
for (const edgeRow of edges) {
|
|
143
|
+
const edge = {
|
|
144
|
+
id: edgeRow.id,
|
|
145
|
+
type: edgeRow.type,
|
|
146
|
+
sourceId: edgeRow.source_id,
|
|
147
|
+
targetId: edgeRow.target_id,
|
|
148
|
+
metadata: edgeRow.metadata ? JSON.parse(edgeRow.metadata) : void 0
|
|
149
|
+
};
|
|
150
|
+
const resolved = this.resolveCall(edge);
|
|
151
|
+
if (resolved.targetNode) {
|
|
152
|
+
resolvedCalls.push(resolved);
|
|
153
|
+
this.updateEdgeTarget(edge.id, resolved.targetNode.id);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return resolvedCalls;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Resolve a single call edge
|
|
160
|
+
*/
|
|
161
|
+
resolveCall(edge) {
|
|
162
|
+
const unresolvedName = edge.metadata?.unresolvedName;
|
|
163
|
+
if (!unresolvedName) {
|
|
164
|
+
return {
|
|
165
|
+
originalEdge: edge,
|
|
166
|
+
targetNode: null,
|
|
167
|
+
confidence: "LOW",
|
|
168
|
+
reason: "No unresolved name in metadata"
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const sourceNode = this.storage.getNode(edge.sourceId);
|
|
172
|
+
if (!sourceNode) {
|
|
173
|
+
return {
|
|
174
|
+
originalEdge: edge,
|
|
175
|
+
targetNode: null,
|
|
176
|
+
confidence: "LOW",
|
|
177
|
+
reason: "Source node not found"
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (!unresolvedName.includes(".")) {
|
|
181
|
+
return this.resolveSimpleCall(unresolvedName, sourceNode, edge);
|
|
182
|
+
}
|
|
183
|
+
return this.resolveMethodCall(unresolvedName, sourceNode, edge);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Resolve simple function call (no dot notation)
|
|
187
|
+
*/
|
|
188
|
+
resolveSimpleCall(functionName, sourceNode, edge) {
|
|
189
|
+
const sameFileNodes = this.storage.getNodesByFile(sourceNode.filePath);
|
|
190
|
+
const localMatch = sameFileNodes.find(
|
|
191
|
+
(n) => (n.type === "function" || n.type === "method") && n.name === functionName
|
|
192
|
+
);
|
|
193
|
+
if (localMatch) {
|
|
194
|
+
return {
|
|
195
|
+
originalEdge: edge,
|
|
196
|
+
targetNode: localMatch,
|
|
197
|
+
confidence: "HIGH",
|
|
198
|
+
reason: "Found in same file"
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const globalMatches = this.storage.searchNodesByName(functionName);
|
|
202
|
+
if (globalMatches.length === 1) {
|
|
203
|
+
return {
|
|
204
|
+
originalEdge: edge,
|
|
205
|
+
targetNode: globalMatches[0],
|
|
206
|
+
confidence: "MEDIUM",
|
|
207
|
+
reason: "Single global match"
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (globalMatches.length > 1) {
|
|
211
|
+
const sourceDir = sourceNode.filePath.split("/").slice(0, -1).join("/");
|
|
212
|
+
const sameDirMatch = globalMatches.find((n) => n.filePath.startsWith(sourceDir));
|
|
213
|
+
if (sameDirMatch) {
|
|
214
|
+
return {
|
|
215
|
+
originalEdge: edge,
|
|
216
|
+
targetNode: sameDirMatch,
|
|
217
|
+
confidence: "MEDIUM",
|
|
218
|
+
reason: "Multiple matches, picked same directory"
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
originalEdge: edge,
|
|
223
|
+
targetNode: globalMatches[0],
|
|
224
|
+
confidence: "LOW",
|
|
225
|
+
reason: `Multiple matches (${globalMatches.length}), picked first`
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
originalEdge: edge,
|
|
230
|
+
targetNode: null,
|
|
231
|
+
confidence: "LOW",
|
|
232
|
+
reason: "No matches found"
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Resolve method call using type information
|
|
237
|
+
* Pattern: obj.method() or self.method()
|
|
238
|
+
*/
|
|
239
|
+
resolveMethodCall(fullName, sourceNode, edge) {
|
|
240
|
+
const parts = fullName.split(".");
|
|
241
|
+
const varName = parts[0];
|
|
242
|
+
const methodName = parts.slice(1).join(".");
|
|
243
|
+
if (varName === "self" || varName === "this") {
|
|
244
|
+
return this.resolveSelfMethodCall(methodName, sourceNode, edge);
|
|
245
|
+
}
|
|
246
|
+
const scopeTypes = this.variableTypes.get(sourceNode.id) || [];
|
|
247
|
+
const varType = scopeTypes.find((v) => v.name === varName);
|
|
248
|
+
if (!varType) {
|
|
249
|
+
return this.resolveSimpleCall(methodName, sourceNode, edge);
|
|
250
|
+
}
|
|
251
|
+
if (!this.hierarchy) {
|
|
252
|
+
return {
|
|
253
|
+
originalEdge: edge,
|
|
254
|
+
targetNode: null,
|
|
255
|
+
confidence: "LOW",
|
|
256
|
+
reason: "Class hierarchy not built"
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const classNode = resolveMethodCall(varType.type, methodName, this.hierarchy);
|
|
260
|
+
if (classNode) {
|
|
261
|
+
const methodNode = this.findMethodInClass(classNode.name, methodName);
|
|
262
|
+
if (methodNode) {
|
|
263
|
+
return {
|
|
264
|
+
originalEdge: edge,
|
|
265
|
+
targetNode: methodNode,
|
|
266
|
+
confidence: "HIGH",
|
|
267
|
+
reason: `Resolved via type tracking: ${varName}: ${varType.type}`
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
originalEdge: edge,
|
|
273
|
+
targetNode: null,
|
|
274
|
+
confidence: "LOW",
|
|
275
|
+
reason: `Type found (${varType.type}) but method not resolved`
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Resolve self.method() or this.method()
|
|
280
|
+
*/
|
|
281
|
+
resolveSelfMethodCall(methodName, sourceNode, edge) {
|
|
282
|
+
const parentClass = sourceNode.metadata?.parentClass;
|
|
283
|
+
if (!parentClass) {
|
|
284
|
+
return {
|
|
285
|
+
originalEdge: edge,
|
|
286
|
+
targetNode: null,
|
|
287
|
+
confidence: "LOW",
|
|
288
|
+
reason: "self/this call but no parent class"
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (!this.hierarchy) {
|
|
292
|
+
return {
|
|
293
|
+
originalEdge: edge,
|
|
294
|
+
targetNode: null,
|
|
295
|
+
confidence: "LOW",
|
|
296
|
+
reason: "Class hierarchy not built"
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const classNode = resolveMethodCall(parentClass, methodName, this.hierarchy);
|
|
300
|
+
if (classNode) {
|
|
301
|
+
const methodNode2 = this.findMethodInClass(classNode.name, methodName);
|
|
302
|
+
if (methodNode2) {
|
|
303
|
+
return {
|
|
304
|
+
originalEdge: edge,
|
|
305
|
+
targetNode: methodNode2,
|
|
306
|
+
confidence: "HIGH",
|
|
307
|
+
reason: `Resolved self.${methodName} in ${parentClass}`
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const sameFileNodes = this.storage.getNodesByFile(sourceNode.filePath);
|
|
312
|
+
const methodNode = sameFileNodes.find(
|
|
313
|
+
(n) => n.type === "method" && n.name === methodName
|
|
314
|
+
);
|
|
315
|
+
if (methodNode) {
|
|
316
|
+
return {
|
|
317
|
+
originalEdge: edge,
|
|
318
|
+
targetNode: methodNode,
|
|
319
|
+
confidence: "MEDIUM",
|
|
320
|
+
reason: "Found method in same file"
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
originalEdge: edge,
|
|
325
|
+
targetNode: null,
|
|
326
|
+
confidence: "LOW",
|
|
327
|
+
reason: `Method ${methodName} not found in ${parentClass}`
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Find method node in a class
|
|
332
|
+
*/
|
|
333
|
+
findMethodInClass(className, methodName) {
|
|
334
|
+
const stmt = this.storage.db.prepare(`
|
|
335
|
+
SELECT * FROM nodes
|
|
336
|
+
WHERE type = 'method'
|
|
337
|
+
AND name = ?
|
|
338
|
+
AND json_extract(metadata, '$.parentClass') = ?
|
|
339
|
+
`);
|
|
340
|
+
const row = stmt.get(methodName, className);
|
|
341
|
+
if (!row) return null;
|
|
342
|
+
return {
|
|
343
|
+
id: row.id,
|
|
344
|
+
type: row.type,
|
|
345
|
+
name: row.name,
|
|
346
|
+
filePath: row.file_path,
|
|
347
|
+
startLine: row.start_line,
|
|
348
|
+
endLine: row.end_line,
|
|
349
|
+
language: row.language,
|
|
350
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Get all nodes from storage
|
|
355
|
+
*/
|
|
356
|
+
getAllNodes() {
|
|
357
|
+
const stmt = this.storage.db.prepare("SELECT * FROM nodes");
|
|
358
|
+
const rows = stmt.all();
|
|
359
|
+
return rows.map((row) => ({
|
|
360
|
+
id: row.id,
|
|
361
|
+
type: row.type,
|
|
362
|
+
name: row.name,
|
|
363
|
+
filePath: row.file_path,
|
|
364
|
+
startLine: row.start_line,
|
|
365
|
+
endLine: row.end_line,
|
|
366
|
+
language: row.language,
|
|
367
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Update edge target in database
|
|
372
|
+
*/
|
|
373
|
+
updateEdgeTarget(edgeId, newTargetId) {
|
|
374
|
+
const stmt = this.storage.db.prepare(`
|
|
375
|
+
UPDATE edges
|
|
376
|
+
SET target_id = ?
|
|
377
|
+
WHERE id = ?
|
|
378
|
+
`);
|
|
379
|
+
stmt.run(newTargetId, edgeId);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Get resolution statistics
|
|
383
|
+
*/
|
|
384
|
+
getStats() {
|
|
385
|
+
const totalStmt = this.storage.db.prepare(`
|
|
386
|
+
SELECT COUNT(*) as count FROM edges WHERE type = 'calls'
|
|
387
|
+
`);
|
|
388
|
+
const totalRow = totalStmt.get();
|
|
389
|
+
const totalCalls = totalRow.count;
|
|
390
|
+
const resolvedStmt = this.storage.db.prepare(`
|
|
391
|
+
SELECT COUNT(*) as count FROM edges
|
|
392
|
+
WHERE type = 'calls' AND target_id NOT LIKE 'ref:%'
|
|
393
|
+
`);
|
|
394
|
+
const resolvedRow = resolvedStmt.get();
|
|
395
|
+
const resolvedCalls = resolvedRow.count;
|
|
396
|
+
const unresolvedCalls = totalCalls - resolvedCalls;
|
|
397
|
+
const resolutionRate = totalCalls > 0 ? resolvedCalls / totalCalls * 100 : 0;
|
|
398
|
+
return {
|
|
399
|
+
totalCalls,
|
|
400
|
+
resolvedCalls,
|
|
401
|
+
unresolvedCalls,
|
|
402
|
+
resolutionRate
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
21
407
|
// src/parsers/base.ts
|
|
22
408
|
var BaseParser = class {
|
|
23
409
|
nodeIdCounter = 0;
|
|
@@ -97,10 +483,13 @@ var TypeScriptParser = class extends BaseParser {
|
|
|
97
483
|
language = "typescript";
|
|
98
484
|
extensions = [".ts", ".tsx"];
|
|
99
485
|
parser;
|
|
486
|
+
treeSitterParser;
|
|
487
|
+
// Expose for Phase 3
|
|
100
488
|
constructor() {
|
|
101
489
|
super();
|
|
102
490
|
this.parser = new Parser();
|
|
103
491
|
this.parser.setLanguage(TypeScript.typescript);
|
|
492
|
+
this.treeSitterParser = this.parser;
|
|
104
493
|
}
|
|
105
494
|
parse(filePath, content) {
|
|
106
495
|
const analysis = this.createEmptyAnalysis(filePath);
|
|
@@ -272,31 +661,38 @@ var TypeScriptParser = class extends BaseParser {
|
|
|
272
661
|
handleClass(node, filePath, analysis, parentId) {
|
|
273
662
|
const nameNode = node.childForFieldName("name");
|
|
274
663
|
if (!nameNode) return;
|
|
664
|
+
const extends_ = [];
|
|
665
|
+
for (const child of node.children) {
|
|
666
|
+
if (child.type === "class_heritage") {
|
|
667
|
+
const extendsClause = child.children.find((c) => c.type === "extends_clause");
|
|
668
|
+
if (extendsClause) {
|
|
669
|
+
const baseClass = extendsClause.children.find((c) => c.type === "identifier");
|
|
670
|
+
if (baseClass) {
|
|
671
|
+
extends_.push(baseClass.text);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
275
676
|
const classNode = this.createNode(
|
|
276
677
|
"class",
|
|
277
678
|
nameNode.text,
|
|
278
679
|
filePath,
|
|
279
680
|
node.startPosition.row + 1,
|
|
280
|
-
node.endPosition.row + 1
|
|
681
|
+
node.endPosition.row + 1,
|
|
682
|
+
{
|
|
683
|
+
extends: extends_
|
|
684
|
+
}
|
|
281
685
|
);
|
|
282
686
|
analysis.nodes.push(classNode);
|
|
283
687
|
analysis.edges.push(
|
|
284
688
|
this.createEdge("contains", parentId, classNode.id)
|
|
285
689
|
);
|
|
286
|
-
for (const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
analysis.edges.push(
|
|
293
|
-
this.createEdge("extends", classNode.id, `ref:${baseClass.text}`, {
|
|
294
|
-
unresolvedName: baseClass.text
|
|
295
|
-
})
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
690
|
+
for (const baseClass of extends_) {
|
|
691
|
+
analysis.edges.push(
|
|
692
|
+
this.createEdge("extends", classNode.id, `ref:${baseClass}`, {
|
|
693
|
+
unresolvedName: baseClass
|
|
694
|
+
})
|
|
695
|
+
);
|
|
300
696
|
}
|
|
301
697
|
const body = node.childForFieldName("body");
|
|
302
698
|
if (body) {
|
|
@@ -310,6 +706,8 @@ var TypeScriptParser = class extends BaseParser {
|
|
|
310
706
|
handleMethod(node, filePath, analysis, parentId) {
|
|
311
707
|
const nameNode = node.childForFieldName("name");
|
|
312
708
|
if (!nameNode) return;
|
|
709
|
+
const parentNode = analysis.nodes.find((n) => n.id === parentId);
|
|
710
|
+
const parentClass = parentNode?.type === "class" ? parentNode.name : void 0;
|
|
313
711
|
const methodNode = this.createNode(
|
|
314
712
|
"method",
|
|
315
713
|
nameNode.text,
|
|
@@ -318,7 +716,8 @@ var TypeScriptParser = class extends BaseParser {
|
|
|
318
716
|
node.endPosition.row + 1,
|
|
319
717
|
{
|
|
320
718
|
static: node.children.some((c) => c.type === "static"),
|
|
321
|
-
async: node.children.some((c) => c.type === "async")
|
|
719
|
+
async: node.children.some((c) => c.type === "async"),
|
|
720
|
+
parentClass
|
|
322
721
|
}
|
|
323
722
|
);
|
|
324
723
|
analysis.nodes.push(methodNode);
|
|
@@ -378,6 +777,7 @@ var JavaScriptParser = class extends TypeScriptParser {
|
|
|
378
777
|
super();
|
|
379
778
|
this.parser = new Parser();
|
|
380
779
|
this.parser.setLanguage(JavaScript);
|
|
780
|
+
this.treeSitterParser = this.parser;
|
|
381
781
|
}
|
|
382
782
|
parse(filePath, content) {
|
|
383
783
|
const sourceCode = typeof content === "string" ? content : String(content);
|
|
@@ -402,10 +802,13 @@ var PythonParser = class extends BaseParser {
|
|
|
402
802
|
language = "python";
|
|
403
803
|
extensions = [".py"];
|
|
404
804
|
parser;
|
|
805
|
+
treeSitterParser;
|
|
806
|
+
// Expose for Phase 3
|
|
405
807
|
constructor() {
|
|
406
808
|
super();
|
|
407
809
|
this.parser = new Parser2();
|
|
408
810
|
this.parser.setLanguage(Python);
|
|
811
|
+
this.treeSitterParser = this.parser;
|
|
409
812
|
}
|
|
410
813
|
parse(filePath, content) {
|
|
411
814
|
const analysis = this.createEmptyAnalysis(filePath);
|
|
@@ -753,6 +1156,8 @@ var PythonParser = class extends BaseParser {
|
|
|
753
1156
|
node.endPosition.row + 1,
|
|
754
1157
|
{
|
|
755
1158
|
bases,
|
|
1159
|
+
extends: bases,
|
|
1160
|
+
// Add extends for consistency with TypeScript
|
|
756
1161
|
isPrivate: name.startsWith("_")
|
|
757
1162
|
}
|
|
758
1163
|
);
|
|
@@ -790,6 +1195,8 @@ var PythonParser = class extends BaseParser {
|
|
|
790
1195
|
const nameNode = node.childForFieldName("name");
|
|
791
1196
|
if (!nameNode) return;
|
|
792
1197
|
const name = nameNode.text;
|
|
1198
|
+
const parentNode = analysis.nodes.find((n) => n.id === classId);
|
|
1199
|
+
const parentClass = parentNode?.type === "class" ? parentNode.name : void 0;
|
|
793
1200
|
const decorators = [];
|
|
794
1201
|
if (node.parent?.type === "decorated_definition") {
|
|
795
1202
|
for (const sibling of node.parent.children) {
|
|
@@ -818,7 +1225,8 @@ var PythonParser = class extends BaseParser {
|
|
|
818
1225
|
classmethod: isClassMethod,
|
|
819
1226
|
property: isProperty,
|
|
820
1227
|
isPrivate: name.startsWith("_"),
|
|
821
|
-
isDunder: name.startsWith("__") && name.endsWith("__")
|
|
1228
|
+
isDunder: name.startsWith("__") && name.endsWith("__"),
|
|
1229
|
+
parentClass
|
|
822
1230
|
}
|
|
823
1231
|
);
|
|
824
1232
|
analysis.nodes.push(methodNode);
|
|
@@ -961,6 +1369,614 @@ function registerAllParsers() {
|
|
|
961
1369
|
}
|
|
962
1370
|
registerAllParsers();
|
|
963
1371
|
|
|
1372
|
+
// src/flow/phase3-analyzer.ts
|
|
1373
|
+
import { readFileSync } from "fs";
|
|
1374
|
+
|
|
1375
|
+
// src/analysis/property-tracker.ts
|
|
1376
|
+
function extractPropertyAccesses(functionNode, scopeId, scopeName, filePath) {
|
|
1377
|
+
const accesses = [];
|
|
1378
|
+
const isJS = /\.(js|ts|jsx|tsx)$/.test(filePath);
|
|
1379
|
+
const isPython = /\.py$/.test(filePath);
|
|
1380
|
+
if (isJS) {
|
|
1381
|
+
extractJavaScriptPropertyAccesses(functionNode, scopeId, scopeName, accesses);
|
|
1382
|
+
} else if (isPython) {
|
|
1383
|
+
extractPythonPropertyAccesses(functionNode, scopeId, scopeName, accesses);
|
|
1384
|
+
}
|
|
1385
|
+
return accesses;
|
|
1386
|
+
}
|
|
1387
|
+
function extractJavaScriptPropertyAccesses(node, scopeId, scopeName, accesses) {
|
|
1388
|
+
traverseNode(node, (current) => {
|
|
1389
|
+
if (current.type === "member_expression") {
|
|
1390
|
+
const access = parseJavaScriptMemberExpression(current, scopeId);
|
|
1391
|
+
if (access) {
|
|
1392
|
+
accesses.push(access);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (current.type === "optional_chain") {
|
|
1396
|
+
const memberExpr = current.children.find((c) => c.type === "member_expression");
|
|
1397
|
+
if (memberExpr) {
|
|
1398
|
+
const access = parseJavaScriptMemberExpression(memberExpr, scopeId);
|
|
1399
|
+
if (access) {
|
|
1400
|
+
access.accessType = "optional";
|
|
1401
|
+
accesses.push(access);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (current.type === "subscript_expression") {
|
|
1406
|
+
const access = parseJavaScriptSubscript(current, scopeId);
|
|
1407
|
+
if (access) {
|
|
1408
|
+
accesses.push(access);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
function parseJavaScriptMemberExpression(node, scopeId) {
|
|
1414
|
+
const propertyPath = [];
|
|
1415
|
+
let objectName = "";
|
|
1416
|
+
let currentNode = node;
|
|
1417
|
+
while (currentNode && currentNode.type === "member_expression") {
|
|
1418
|
+
const propertyNode = currentNode.childForFieldName("property");
|
|
1419
|
+
if (propertyNode) {
|
|
1420
|
+
propertyPath.unshift(propertyNode.text);
|
|
1421
|
+
}
|
|
1422
|
+
const objectNode = currentNode.childForFieldName("object");
|
|
1423
|
+
if (!objectNode) break;
|
|
1424
|
+
if (objectNode.type === "identifier") {
|
|
1425
|
+
objectName = objectNode.text;
|
|
1426
|
+
break;
|
|
1427
|
+
} else if (objectNode.type === "member_expression") {
|
|
1428
|
+
currentNode = objectNode;
|
|
1429
|
+
} else {
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
if (!objectName || isCommonJavaScriptObject(objectName)) {
|
|
1434
|
+
return null;
|
|
1435
|
+
}
|
|
1436
|
+
if (propertyPath.length === 0) {
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
return {
|
|
1440
|
+
objectName,
|
|
1441
|
+
propertyPath,
|
|
1442
|
+
fullPath: `${objectName}.${propertyPath.join(".")}`,
|
|
1443
|
+
line: node.startPosition.row + 1,
|
|
1444
|
+
scopeId,
|
|
1445
|
+
accessType: "dot"
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
function parseJavaScriptSubscript(node, scopeId) {
|
|
1449
|
+
const objectNode = node.childForFieldName("object");
|
|
1450
|
+
const indexNode = node.childForFieldName("index");
|
|
1451
|
+
if (!objectNode || !indexNode) return null;
|
|
1452
|
+
if (indexNode.type !== "string") return null;
|
|
1453
|
+
const objectName = objectNode.text;
|
|
1454
|
+
const propertyName = indexNode.text.replace(/['"]/g, "");
|
|
1455
|
+
if (isCommonJavaScriptObject(objectName)) {
|
|
1456
|
+
return null;
|
|
1457
|
+
}
|
|
1458
|
+
return {
|
|
1459
|
+
objectName,
|
|
1460
|
+
propertyPath: [propertyName],
|
|
1461
|
+
fullPath: `${objectName}.${propertyName}`,
|
|
1462
|
+
line: node.startPosition.row + 1,
|
|
1463
|
+
scopeId,
|
|
1464
|
+
accessType: "bracket"
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
function extractPythonPropertyAccesses(node, scopeId, scopeName, accesses) {
|
|
1468
|
+
traverseNode(node, (current) => {
|
|
1469
|
+
if (current.type === "attribute") {
|
|
1470
|
+
const access = parsePythonAttribute(current, scopeId);
|
|
1471
|
+
if (access) {
|
|
1472
|
+
accesses.push(access);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (current.type === "subscript") {
|
|
1476
|
+
const access = parsePythonSubscript(current, scopeId);
|
|
1477
|
+
if (access) {
|
|
1478
|
+
accesses.push(access);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
function parsePythonAttribute(node, scopeId) {
|
|
1484
|
+
const propertyPath = [];
|
|
1485
|
+
let objectName = "";
|
|
1486
|
+
let currentNode = node;
|
|
1487
|
+
while (currentNode && currentNode.type === "attribute") {
|
|
1488
|
+
const attrNode = currentNode.childForFieldName("attribute");
|
|
1489
|
+
if (attrNode) {
|
|
1490
|
+
propertyPath.unshift(attrNode.text);
|
|
1491
|
+
}
|
|
1492
|
+
const objectNode = currentNode.childForFieldName("object");
|
|
1493
|
+
if (!objectNode) break;
|
|
1494
|
+
if (objectNode.type === "identifier") {
|
|
1495
|
+
objectName = objectNode.text;
|
|
1496
|
+
break;
|
|
1497
|
+
} else if (objectNode.type === "attribute") {
|
|
1498
|
+
currentNode = objectNode;
|
|
1499
|
+
} else {
|
|
1500
|
+
return null;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (!objectName || isCommonPythonObject(objectName)) {
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
if (propertyPath.length === 0) {
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
return {
|
|
1510
|
+
objectName,
|
|
1511
|
+
propertyPath,
|
|
1512
|
+
fullPath: `${objectName}.${propertyPath.join(".")}`,
|
|
1513
|
+
line: node.startPosition.row + 1,
|
|
1514
|
+
scopeId,
|
|
1515
|
+
accessType: "dot"
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
function parsePythonSubscript(node, scopeId) {
|
|
1519
|
+
const valueNode = node.childForFieldName("value");
|
|
1520
|
+
const subscriptChildren = node.children.filter(
|
|
1521
|
+
(c) => c.type === "string" || c.type === "identifier"
|
|
1522
|
+
);
|
|
1523
|
+
if (!valueNode || subscriptChildren.length === 0) return null;
|
|
1524
|
+
const keyNode = subscriptChildren.find((c) => c.type === "string");
|
|
1525
|
+
if (!keyNode) return null;
|
|
1526
|
+
const objectName = valueNode.text;
|
|
1527
|
+
const stringContent = keyNode.children.find((c) => c.type === "string_content");
|
|
1528
|
+
const propertyName = stringContent ? stringContent.text : keyNode.text.replace(/['"]/g, "");
|
|
1529
|
+
if (isCommonPythonObject(objectName)) {
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
return {
|
|
1533
|
+
objectName,
|
|
1534
|
+
propertyPath: [propertyName],
|
|
1535
|
+
fullPath: `${objectName}.${propertyName}`,
|
|
1536
|
+
line: node.startPosition.row + 1,
|
|
1537
|
+
scopeId,
|
|
1538
|
+
accessType: "bracket"
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
function traverseNode(node, visitor) {
|
|
1542
|
+
visitor(node);
|
|
1543
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1544
|
+
const child = node.child(i);
|
|
1545
|
+
if (child) {
|
|
1546
|
+
traverseNode(child, visitor);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
function isCommonJavaScriptObject(name) {
|
|
1551
|
+
const commonObjects = /* @__PURE__ */ new Set([
|
|
1552
|
+
// Global objects
|
|
1553
|
+
"console",
|
|
1554
|
+
"window",
|
|
1555
|
+
"document",
|
|
1556
|
+
"process",
|
|
1557
|
+
"global",
|
|
1558
|
+
// Common libraries
|
|
1559
|
+
"Math",
|
|
1560
|
+
"JSON",
|
|
1561
|
+
"Date",
|
|
1562
|
+
"Array",
|
|
1563
|
+
"Object",
|
|
1564
|
+
"String",
|
|
1565
|
+
"Number",
|
|
1566
|
+
// Keywords
|
|
1567
|
+
"this",
|
|
1568
|
+
"super",
|
|
1569
|
+
// Common module patterns
|
|
1570
|
+
"exports",
|
|
1571
|
+
"module",
|
|
1572
|
+
"require"
|
|
1573
|
+
]);
|
|
1574
|
+
return commonObjects.has(name);
|
|
1575
|
+
}
|
|
1576
|
+
function isCommonPythonObject(name) {
|
|
1577
|
+
const commonObjects = /* @__PURE__ */ new Set([
|
|
1578
|
+
// Keywords
|
|
1579
|
+
"self",
|
|
1580
|
+
"cls",
|
|
1581
|
+
// Built-in types
|
|
1582
|
+
"str",
|
|
1583
|
+
"int",
|
|
1584
|
+
"float",
|
|
1585
|
+
"dict",
|
|
1586
|
+
"list",
|
|
1587
|
+
"tuple",
|
|
1588
|
+
"set",
|
|
1589
|
+
// Common modules
|
|
1590
|
+
"os",
|
|
1591
|
+
"sys",
|
|
1592
|
+
"json",
|
|
1593
|
+
"math",
|
|
1594
|
+
"datetime"
|
|
1595
|
+
]);
|
|
1596
|
+
return commonObjects.has(name);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/analysis/return-analyzer.ts
|
|
1600
|
+
function analyzeReturnStatements(functionNode, functionId, functionName, filePath) {
|
|
1601
|
+
const returns = [];
|
|
1602
|
+
const isJS = /\.(js|ts|jsx|tsx)$/.test(filePath);
|
|
1603
|
+
const isPython = /\.py$/.test(filePath);
|
|
1604
|
+
if (isJS) {
|
|
1605
|
+
extractJavaScriptReturns(functionNode, functionId, functionName, returns);
|
|
1606
|
+
} else if (isPython) {
|
|
1607
|
+
extractPythonReturns(functionNode, functionId, functionName, returns);
|
|
1608
|
+
}
|
|
1609
|
+
return returns;
|
|
1610
|
+
}
|
|
1611
|
+
function extractJavaScriptReturns(node, functionId, functionName, returns) {
|
|
1612
|
+
traverseNode2(node, (current) => {
|
|
1613
|
+
if (current.type === "return_statement") {
|
|
1614
|
+
const returnNode = current.children.find(
|
|
1615
|
+
(c) => c.type !== "return" && c.type !== ";"
|
|
1616
|
+
);
|
|
1617
|
+
if (returnNode) {
|
|
1618
|
+
const returnInfo = parseJavaScriptReturnValue(
|
|
1619
|
+
returnNode,
|
|
1620
|
+
functionId,
|
|
1621
|
+
functionName
|
|
1622
|
+
);
|
|
1623
|
+
if (returnInfo) {
|
|
1624
|
+
returns.push(returnInfo);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
function parseJavaScriptReturnValue(returnNode, functionId, functionName) {
|
|
1631
|
+
const line = returnNode.startPosition.row + 1;
|
|
1632
|
+
const returnExpression = returnNode.text;
|
|
1633
|
+
let returnType = null;
|
|
1634
|
+
let objectShape = null;
|
|
1635
|
+
if (returnNode.type === "object") {
|
|
1636
|
+
objectShape = extractJavaScriptObjectShape(returnNode);
|
|
1637
|
+
returnType = "object";
|
|
1638
|
+
} else if (returnNode.type === "array") {
|
|
1639
|
+
returnType = "array";
|
|
1640
|
+
objectShape = {
|
|
1641
|
+
properties: [],
|
|
1642
|
+
propertyTypes: /* @__PURE__ */ new Map(),
|
|
1643
|
+
nestedShapes: /* @__PURE__ */ new Map(),
|
|
1644
|
+
isArray: true,
|
|
1645
|
+
isDictionary: false,
|
|
1646
|
+
isOptional: false
|
|
1647
|
+
};
|
|
1648
|
+
} else if (returnNode.type === "new_expression") {
|
|
1649
|
+
const constructorNode = returnNode.childForFieldName("constructor");
|
|
1650
|
+
if (constructorNode) {
|
|
1651
|
+
returnType = constructorNode.text;
|
|
1652
|
+
}
|
|
1653
|
+
} else if (returnNode.type === "identifier") {
|
|
1654
|
+
returnType = `@var:${returnNode.text}`;
|
|
1655
|
+
} else if (returnNode.type === "call_expression") {
|
|
1656
|
+
const funcNode = returnNode.childForFieldName("function");
|
|
1657
|
+
if (funcNode) {
|
|
1658
|
+
returnType = `@call:${funcNode.text}`;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return {
|
|
1662
|
+
functionId,
|
|
1663
|
+
functionName,
|
|
1664
|
+
returnType,
|
|
1665
|
+
objectShape,
|
|
1666
|
+
line,
|
|
1667
|
+
returnExpression
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
function extractJavaScriptObjectShape(objectNode) {
|
|
1671
|
+
const properties = [];
|
|
1672
|
+
const propertyTypes = /* @__PURE__ */ new Map();
|
|
1673
|
+
const nestedShapes = /* @__PURE__ */ new Map();
|
|
1674
|
+
for (const child of objectNode.children) {
|
|
1675
|
+
if (child.type === "pair") {
|
|
1676
|
+
const keyNode = child.childForFieldName("key");
|
|
1677
|
+
const valueNode = child.childForFieldName("value");
|
|
1678
|
+
if (keyNode) {
|
|
1679
|
+
const propertyName = keyNode.text.replace(/['"]/g, "");
|
|
1680
|
+
properties.push(propertyName);
|
|
1681
|
+
if (valueNode) {
|
|
1682
|
+
const propertyType = inferJavaScriptType(valueNode);
|
|
1683
|
+
propertyTypes.set(propertyName, propertyType);
|
|
1684
|
+
if (valueNode.type === "object") {
|
|
1685
|
+
const nestedShape = extractJavaScriptObjectShape(valueNode);
|
|
1686
|
+
nestedShapes.set(propertyName, nestedShape);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
return {
|
|
1693
|
+
properties,
|
|
1694
|
+
propertyTypes,
|
|
1695
|
+
nestedShapes,
|
|
1696
|
+
isArray: false,
|
|
1697
|
+
isDictionary: false,
|
|
1698
|
+
isOptional: false
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
function inferJavaScriptType(valueNode) {
|
|
1702
|
+
switch (valueNode.type) {
|
|
1703
|
+
case "number":
|
|
1704
|
+
return "number";
|
|
1705
|
+
case "string":
|
|
1706
|
+
case "template_string":
|
|
1707
|
+
return "string";
|
|
1708
|
+
case "true":
|
|
1709
|
+
case "false":
|
|
1710
|
+
return "boolean";
|
|
1711
|
+
case "null":
|
|
1712
|
+
return "null";
|
|
1713
|
+
case "undefined":
|
|
1714
|
+
return "undefined";
|
|
1715
|
+
case "array":
|
|
1716
|
+
return "array";
|
|
1717
|
+
case "object":
|
|
1718
|
+
return "object";
|
|
1719
|
+
case "arrow_function":
|
|
1720
|
+
case "function_expression":
|
|
1721
|
+
return "function";
|
|
1722
|
+
case "identifier":
|
|
1723
|
+
return `@var:${valueNode.text}`;
|
|
1724
|
+
case "call_expression":
|
|
1725
|
+
return "@call";
|
|
1726
|
+
default:
|
|
1727
|
+
return "unknown";
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function extractPythonReturns(node, functionId, functionName, returns) {
|
|
1731
|
+
traverseNode2(node, (current) => {
|
|
1732
|
+
if (current.type === "return_statement") {
|
|
1733
|
+
const returnNode = current.children.find((c) => c.type !== "return");
|
|
1734
|
+
if (returnNode) {
|
|
1735
|
+
const returnInfo = parsePythonReturnValue(
|
|
1736
|
+
returnNode,
|
|
1737
|
+
functionId,
|
|
1738
|
+
functionName
|
|
1739
|
+
);
|
|
1740
|
+
if (returnInfo) {
|
|
1741
|
+
returns.push(returnInfo);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
function parsePythonReturnValue(returnNode, functionId, functionName) {
|
|
1748
|
+
const line = returnNode.startPosition.row + 1;
|
|
1749
|
+
const returnExpression = returnNode.text;
|
|
1750
|
+
let returnType = null;
|
|
1751
|
+
let objectShape = null;
|
|
1752
|
+
if (returnNode.type === "dictionary") {
|
|
1753
|
+
objectShape = extractPythonDictionaryShape(returnNode);
|
|
1754
|
+
returnType = "dict";
|
|
1755
|
+
} else if (returnNode.type === "list") {
|
|
1756
|
+
returnType = "list";
|
|
1757
|
+
objectShape = {
|
|
1758
|
+
properties: [],
|
|
1759
|
+
propertyTypes: /* @__PURE__ */ new Map(),
|
|
1760
|
+
nestedShapes: /* @__PURE__ */ new Map(),
|
|
1761
|
+
isArray: true,
|
|
1762
|
+
isDictionary: false,
|
|
1763
|
+
isOptional: false
|
|
1764
|
+
};
|
|
1765
|
+
} else if (returnNode.type === "call") {
|
|
1766
|
+
const funcNode = returnNode.childForFieldName("function");
|
|
1767
|
+
if (funcNode) {
|
|
1768
|
+
const funcName = funcNode.text;
|
|
1769
|
+
if (funcName[0] === funcName[0].toUpperCase()) {
|
|
1770
|
+
returnType = funcName;
|
|
1771
|
+
} else {
|
|
1772
|
+
returnType = `@call:${funcName}`;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
} else if (returnNode.type === "identifier") {
|
|
1776
|
+
returnType = `@var:${returnNode.text}`;
|
|
1777
|
+
} else if (returnNode.type === "list_comprehension") {
|
|
1778
|
+
returnType = "list";
|
|
1779
|
+
objectShape = {
|
|
1780
|
+
properties: [],
|
|
1781
|
+
propertyTypes: /* @__PURE__ */ new Map(),
|
|
1782
|
+
nestedShapes: /* @__PURE__ */ new Map(),
|
|
1783
|
+
isArray: true,
|
|
1784
|
+
isDictionary: false,
|
|
1785
|
+
isOptional: false
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
return {
|
|
1789
|
+
functionId,
|
|
1790
|
+
functionName,
|
|
1791
|
+
returnType,
|
|
1792
|
+
objectShape,
|
|
1793
|
+
line,
|
|
1794
|
+
returnExpression
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
function extractPythonDictionaryShape(dictNode) {
|
|
1798
|
+
const properties = [];
|
|
1799
|
+
const propertyTypes = /* @__PURE__ */ new Map();
|
|
1800
|
+
const nestedShapes = /* @__PURE__ */ new Map();
|
|
1801
|
+
for (const child of dictNode.children) {
|
|
1802
|
+
if (child.type === "pair") {
|
|
1803
|
+
const keyNode = child.children.find((c) => c.type === "string");
|
|
1804
|
+
const valueNode = child.children.find(
|
|
1805
|
+
(c) => c.type !== "string" && c.type !== ":"
|
|
1806
|
+
);
|
|
1807
|
+
if (keyNode) {
|
|
1808
|
+
const stringContent = keyNode.children.find(
|
|
1809
|
+
(c) => c.type === "string_content"
|
|
1810
|
+
);
|
|
1811
|
+
const propertyName = stringContent ? stringContent.text : keyNode.text.replace(/['"]/g, "");
|
|
1812
|
+
properties.push(propertyName);
|
|
1813
|
+
if (valueNode) {
|
|
1814
|
+
const propertyType = inferPythonType(valueNode);
|
|
1815
|
+
propertyTypes.set(propertyName, propertyType);
|
|
1816
|
+
if (valueNode.type === "dictionary") {
|
|
1817
|
+
const nestedShape = extractPythonDictionaryShape(valueNode);
|
|
1818
|
+
nestedShapes.set(propertyName, nestedShape);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return {
|
|
1825
|
+
properties,
|
|
1826
|
+
propertyTypes,
|
|
1827
|
+
nestedShapes,
|
|
1828
|
+
isArray: false,
|
|
1829
|
+
isDictionary: true,
|
|
1830
|
+
isOptional: false
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
function inferPythonType(valueNode) {
|
|
1834
|
+
switch (valueNode.type) {
|
|
1835
|
+
case "integer":
|
|
1836
|
+
case "float":
|
|
1837
|
+
return "number";
|
|
1838
|
+
case "string":
|
|
1839
|
+
return "string";
|
|
1840
|
+
case "true":
|
|
1841
|
+
case "false":
|
|
1842
|
+
return "boolean";
|
|
1843
|
+
case "none":
|
|
1844
|
+
return "None";
|
|
1845
|
+
case "list":
|
|
1846
|
+
return "list";
|
|
1847
|
+
case "dictionary":
|
|
1848
|
+
return "dict";
|
|
1849
|
+
case "lambda":
|
|
1850
|
+
return "function";
|
|
1851
|
+
case "identifier":
|
|
1852
|
+
return `@var:${valueNode.text}`;
|
|
1853
|
+
case "call":
|
|
1854
|
+
return "@call";
|
|
1855
|
+
default:
|
|
1856
|
+
return "unknown";
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
function traverseNode2(node, visitor) {
|
|
1860
|
+
visitor(node);
|
|
1861
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1862
|
+
const child = node.child(i);
|
|
1863
|
+
if (child) {
|
|
1864
|
+
traverseNode2(child, visitor);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// src/flow/storage-queries-phase3.ts
|
|
1870
|
+
import { randomUUID } from "crypto";
|
|
1871
|
+
function storePropertyAccesses(db, accesses) {
|
|
1872
|
+
const stmt = db.prepare(`
|
|
1873
|
+
INSERT OR REPLACE INTO property_accesses
|
|
1874
|
+
(id, scope_node_id, object_name, property_path, full_path, access_type, file_path, line)
|
|
1875
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
1876
|
+
`);
|
|
1877
|
+
const insert = db.transaction((accesses2) => {
|
|
1878
|
+
for (const access of accesses2) {
|
|
1879
|
+
stmt.run(
|
|
1880
|
+
randomUUID(),
|
|
1881
|
+
access.scopeId,
|
|
1882
|
+
access.objectName,
|
|
1883
|
+
JSON.stringify(access.propertyPath),
|
|
1884
|
+
access.fullPath,
|
|
1885
|
+
access.accessType,
|
|
1886
|
+
access.filePath || "",
|
|
1887
|
+
access.line
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
});
|
|
1891
|
+
insert(accesses);
|
|
1892
|
+
}
|
|
1893
|
+
function storeReturnInfo(db, returns) {
|
|
1894
|
+
const stmt = db.prepare(`
|
|
1895
|
+
INSERT OR REPLACE INTO return_shapes
|
|
1896
|
+
(id, function_id, return_type, object_shape, line, return_expression)
|
|
1897
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1898
|
+
`);
|
|
1899
|
+
const insert = db.transaction((returns2) => {
|
|
1900
|
+
for (const ret of returns2) {
|
|
1901
|
+
stmt.run(
|
|
1902
|
+
randomUUID(),
|
|
1903
|
+
ret.functionId,
|
|
1904
|
+
ret.returnType,
|
|
1905
|
+
ret.objectShape ? JSON.stringify(serializeObjectShape(ret.objectShape)) : null,
|
|
1906
|
+
ret.line,
|
|
1907
|
+
ret.returnExpression
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
});
|
|
1911
|
+
insert(returns);
|
|
1912
|
+
}
|
|
1913
|
+
function serializeObjectShape(shape) {
|
|
1914
|
+
return {
|
|
1915
|
+
properties: shape.properties,
|
|
1916
|
+
propertyTypes: Array.from(shape.propertyTypes.entries()),
|
|
1917
|
+
nestedShapes: Array.from(shape.nestedShapes.entries()).map(([key, nested]) => [
|
|
1918
|
+
key,
|
|
1919
|
+
serializeObjectShape(nested)
|
|
1920
|
+
]),
|
|
1921
|
+
isArray: shape.isArray,
|
|
1922
|
+
isDictionary: shape.isDictionary,
|
|
1923
|
+
isOptional: shape.isOptional
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// src/flow/phase3-analyzer.ts
|
|
1928
|
+
function analyzeFileForPhase3(filePath, functionNodes, db) {
|
|
1929
|
+
try {
|
|
1930
|
+
const language = getLanguageFromPath(filePath);
|
|
1931
|
+
if (!language) return;
|
|
1932
|
+
const parserInstance = parserRegistry.getByLanguage(language);
|
|
1933
|
+
if (!parserInstance || !parserInstance.treeSitterParser) return;
|
|
1934
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1935
|
+
const tree = parserInstance.treeSitterParser.parse(content);
|
|
1936
|
+
const rootNode = tree.rootNode;
|
|
1937
|
+
for (const func of functionNodes) {
|
|
1938
|
+
const funcNode = findNodeAtLine(rootNode, func.startLine, func.endLine);
|
|
1939
|
+
if (!funcNode) continue;
|
|
1940
|
+
const accesses = extractPropertyAccesses(funcNode, func.id, func.name, filePath);
|
|
1941
|
+
if (accesses.length > 0) {
|
|
1942
|
+
const accessesWithFile = accesses.map((a) => ({ ...a, filePath }));
|
|
1943
|
+
storePropertyAccesses(db, accessesWithFile);
|
|
1944
|
+
}
|
|
1945
|
+
const returns = analyzeReturnStatements(funcNode, func.id, func.name, filePath);
|
|
1946
|
+
if (returns.length > 0) {
|
|
1947
|
+
storeReturnInfo(db, returns);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
function getLanguageFromPath(filePath) {
|
|
1954
|
+
if (filePath.endsWith(".py")) return "python";
|
|
1955
|
+
if (filePath.endsWith(".js") || filePath.endsWith(".jsx")) return "javascript";
|
|
1956
|
+
if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "typescript";
|
|
1957
|
+
return null;
|
|
1958
|
+
}
|
|
1959
|
+
function findNodeAtLine(node, startLine, endLine) {
|
|
1960
|
+
const nodeStart = node.startPosition.row + 1;
|
|
1961
|
+
const nodeEnd = node.endPosition.row + 1;
|
|
1962
|
+
if (nodeStart === startLine && nodeEnd === endLine) {
|
|
1963
|
+
return node;
|
|
1964
|
+
}
|
|
1965
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
1966
|
+
const child = node.child(i);
|
|
1967
|
+
if (child) {
|
|
1968
|
+
const result = findNodeAtLine(child, startLine, endLine);
|
|
1969
|
+
if (result) return result;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
if (nodeStart <= startLine && nodeEnd >= endLine) {
|
|
1973
|
+
if (node.type === "function_definition" || node.type === "function_declaration" || node.type === "method_definition" || node.type === "arrow_function" || node.type === "function_expression") {
|
|
1974
|
+
return node;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
964
1980
|
// src/flow/builder.ts
|
|
965
1981
|
var FlowBuilder = class {
|
|
966
1982
|
storage;
|
|
@@ -988,7 +2004,7 @@ var FlowBuilder = class {
|
|
|
988
2004
|
const filesToIndex = [];
|
|
989
2005
|
let skipped = 0;
|
|
990
2006
|
for (const filePath of allFiles) {
|
|
991
|
-
const content =
|
|
2007
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
992
2008
|
const hash = this.hashContent(content);
|
|
993
2009
|
if (!this.config.forceReindex) {
|
|
994
2010
|
const existingHash = this.storage.getFileHash(filePath);
|
|
@@ -1021,6 +2037,12 @@ var FlowBuilder = class {
|
|
|
1021
2037
|
currentFile: ""
|
|
1022
2038
|
});
|
|
1023
2039
|
this.resolveAllCalls();
|
|
2040
|
+
const enhancedResolver = new EnhancedResolver(this.storage);
|
|
2041
|
+
enhancedResolver.buildHierarchy();
|
|
2042
|
+
const enhancedResolved = enhancedResolver.resolveAllCalls();
|
|
2043
|
+
if (enhancedResolved.length > 0) {
|
|
2044
|
+
console.log(`\u2728 Enhanced resolver: resolved ${enhancedResolved.length} additional calls`);
|
|
2045
|
+
}
|
|
1024
2046
|
const stats = this.storage.getStats();
|
|
1025
2047
|
this.emitProgress({
|
|
1026
2048
|
phase: "complete",
|
|
@@ -1066,7 +2088,7 @@ var FlowBuilder = class {
|
|
|
1066
2088
|
}
|
|
1067
2089
|
async parseAndStore(filePath, hash, rootPath) {
|
|
1068
2090
|
const language = this.getLanguage(filePath);
|
|
1069
|
-
const content =
|
|
2091
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
1070
2092
|
const parser = parserRegistry.getByLanguage(language);
|
|
1071
2093
|
if (!parser) {
|
|
1072
2094
|
throw new Error(`No parser found for language: ${language}`);
|
|
@@ -1103,6 +2125,15 @@ var FlowBuilder = class {
|
|
|
1103
2125
|
);
|
|
1104
2126
|
}
|
|
1105
2127
|
}
|
|
2128
|
+
const functionNodes = analysis.nodes.filter((n) => n.type === "function" || n.type === "method").map((n) => ({
|
|
2129
|
+
id: n.id,
|
|
2130
|
+
name: n.name,
|
|
2131
|
+
startLine: n.startLine,
|
|
2132
|
+
endLine: n.endLine
|
|
2133
|
+
}));
|
|
2134
|
+
if (functionNodes.length > 0) {
|
|
2135
|
+
analyzeFileForPhase3(filePath, functionNodes, this.storage.db);
|
|
2136
|
+
}
|
|
1106
2137
|
if (analysis.databaseOperations) {
|
|
1107
2138
|
for (const dbOp of analysis.databaseOperations) {
|
|
1108
2139
|
this.storage.insertDatabaseOperation({
|
|
@@ -1382,9 +2413,90 @@ function getGitStatus(rootPath) {
|
|
|
1382
2413
|
}
|
|
1383
2414
|
|
|
1384
2415
|
// src/analysis/imports.ts
|
|
1385
|
-
import { readFileSync as
|
|
2416
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2417
|
+
function isJavaScriptBuiltin(identifier) {
|
|
2418
|
+
const builtins = /* @__PURE__ */ new Set([
|
|
2419
|
+
"Object",
|
|
2420
|
+
"Array",
|
|
2421
|
+
"String",
|
|
2422
|
+
"Number",
|
|
2423
|
+
"Boolean",
|
|
2424
|
+
"Date",
|
|
2425
|
+
"RegExp",
|
|
2426
|
+
"Error",
|
|
2427
|
+
"TypeError",
|
|
2428
|
+
"SyntaxError",
|
|
2429
|
+
"ReferenceError",
|
|
2430
|
+
"Promise",
|
|
2431
|
+
"Map",
|
|
2432
|
+
"Set",
|
|
2433
|
+
"WeakMap",
|
|
2434
|
+
"WeakSet",
|
|
2435
|
+
"JSON",
|
|
2436
|
+
"Math",
|
|
2437
|
+
"Infinity",
|
|
2438
|
+
"NaN",
|
|
2439
|
+
"undefined",
|
|
2440
|
+
"Function",
|
|
2441
|
+
"Symbol",
|
|
2442
|
+
"Proxy",
|
|
2443
|
+
"Reflect",
|
|
2444
|
+
"Int8Array",
|
|
2445
|
+
"Uint8Array",
|
|
2446
|
+
"Int16Array",
|
|
2447
|
+
"Uint16Array",
|
|
2448
|
+
"Int32Array",
|
|
2449
|
+
"Uint32Array",
|
|
2450
|
+
"Float32Array",
|
|
2451
|
+
"Float64Array",
|
|
2452
|
+
"BigInt",
|
|
2453
|
+
"ArrayBuffer",
|
|
2454
|
+
"DataView",
|
|
2455
|
+
"Intl",
|
|
2456
|
+
"WebAssembly",
|
|
2457
|
+
"Buffer",
|
|
2458
|
+
"URL",
|
|
2459
|
+
"URLSearchParams",
|
|
2460
|
+
"console",
|
|
2461
|
+
"process",
|
|
2462
|
+
"global",
|
|
2463
|
+
"window",
|
|
2464
|
+
"document",
|
|
2465
|
+
// Common keywords that look like types
|
|
2466
|
+
"TRUE",
|
|
2467
|
+
"FALSE",
|
|
2468
|
+
"NULL"
|
|
2469
|
+
]);
|
|
2470
|
+
return builtins.has(identifier);
|
|
2471
|
+
}
|
|
2472
|
+
function isPythonBuiltin(identifier) {
|
|
2473
|
+
const builtins = /* @__PURE__ */ new Set([
|
|
2474
|
+
"True",
|
|
2475
|
+
"False",
|
|
2476
|
+
"None",
|
|
2477
|
+
"Exception",
|
|
2478
|
+
"BaseException",
|
|
2479
|
+
"ValueError",
|
|
2480
|
+
"TypeError",
|
|
2481
|
+
"KeyError",
|
|
2482
|
+
"AttributeError",
|
|
2483
|
+
"IndexError",
|
|
2484
|
+
"RuntimeError",
|
|
2485
|
+
"NotImplementedError",
|
|
2486
|
+
"Dict",
|
|
2487
|
+
"List",
|
|
2488
|
+
"Tuple",
|
|
2489
|
+
"Set",
|
|
2490
|
+
"FrozenSet",
|
|
2491
|
+
"Optional",
|
|
2492
|
+
"Union",
|
|
2493
|
+
"Any",
|
|
2494
|
+
"Callable"
|
|
2495
|
+
]);
|
|
2496
|
+
return builtins.has(identifier);
|
|
2497
|
+
}
|
|
1386
2498
|
function analyzeJavaScriptImports(filePath) {
|
|
1387
|
-
const content =
|
|
2499
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
1388
2500
|
const lines = content.split("\n");
|
|
1389
2501
|
const imports = /* @__PURE__ */ new Map();
|
|
1390
2502
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1423,19 +2535,52 @@ function analyzeJavaScriptImports(filePath) {
|
|
|
1423
2535
|
}
|
|
1424
2536
|
}
|
|
1425
2537
|
const missing = [];
|
|
1426
|
-
const
|
|
1427
|
-
for (
|
|
1428
|
-
|
|
2538
|
+
const localDefinitions = /* @__PURE__ */ new Set();
|
|
2539
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2540
|
+
const line = lines[i];
|
|
2541
|
+
const classMatch = line.match(/^(?:export\s+)?class\s+([A-Z][a-zA-Z0-9]*)/);
|
|
2542
|
+
if (classMatch) {
|
|
2543
|
+
localDefinitions.add(classMatch[1]);
|
|
2544
|
+
}
|
|
2545
|
+
const funcMatch = line.match(/^(?:export\s+)?(?:async\s+)?function\s+([A-Z][a-zA-Z0-9]*)/);
|
|
2546
|
+
if (funcMatch) {
|
|
2547
|
+
localDefinitions.add(funcMatch[1]);
|
|
2548
|
+
}
|
|
2549
|
+
const constMatch = line.match(/^(?:export\s+)?const\s+([A-Z][a-z][a-zA-Z0-9]*)\s*=/);
|
|
2550
|
+
if (constMatch) {
|
|
2551
|
+
localDefinitions.add(constMatch[1]);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
const potentialImports = /* @__PURE__ */ new Set();
|
|
2555
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2556
|
+
let line = lines[i];
|
|
2557
|
+
if (line.trim().startsWith("import ") || line.trim().startsWith("//") || line.trim().startsWith("*") || line.trim().startsWith("#")) {
|
|
2558
|
+
continue;
|
|
2559
|
+
}
|
|
2560
|
+
line = line.replace(/\/\/.*$/g, "");
|
|
2561
|
+
line = line.replace(/\/\*.*?\*\//g, "");
|
|
2562
|
+
line = line.replace(/[`"'].*?[`"']/g, "");
|
|
2563
|
+
const matches = line.matchAll(/(?<!\.)([A-Z][a-z][a-zA-Z0-9]*)(?!\.)/g);
|
|
2564
|
+
for (const match of matches) {
|
|
2565
|
+
const identifier = match[1];
|
|
2566
|
+
if (!isJavaScriptBuiltin(identifier)) {
|
|
2567
|
+
potentialImports.add(identifier);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
for (const identifier of potentialImports) {
|
|
2572
|
+
if (!imports.has(identifier) && !localDefinitions.has(identifier)) {
|
|
1429
2573
|
const usageLines = [];
|
|
1430
2574
|
for (let i = 0; i < lines.length; i++) {
|
|
1431
2575
|
const line = lines[i];
|
|
1432
|
-
|
|
2576
|
+
const regex = new RegExp(`\\b${identifier}\\b`);
|
|
2577
|
+
if (regex.test(line) && !line.trim().startsWith("//")) {
|
|
1433
2578
|
usageLines.push(i + 1);
|
|
1434
2579
|
}
|
|
1435
2580
|
}
|
|
1436
2581
|
if (usageLines.length > 0) {
|
|
1437
2582
|
missing.push({
|
|
1438
|
-
identifier
|
|
2583
|
+
identifier,
|
|
1439
2584
|
line: usageLines[0],
|
|
1440
2585
|
type: "missing",
|
|
1441
2586
|
usedAt: usageLines
|
|
@@ -1446,7 +2591,7 @@ function analyzeJavaScriptImports(filePath) {
|
|
|
1446
2591
|
return { missing, unused };
|
|
1447
2592
|
}
|
|
1448
2593
|
function analyzePythonImports(filePath) {
|
|
1449
|
-
const content =
|
|
2594
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
1450
2595
|
const lines = content.split("\n");
|
|
1451
2596
|
const imports = /* @__PURE__ */ new Map();
|
|
1452
2597
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1484,7 +2629,56 @@ function analyzePythonImports(filePath) {
|
|
|
1484
2629
|
unused.push({ identifier, line, type: "unused" });
|
|
1485
2630
|
}
|
|
1486
2631
|
}
|
|
1487
|
-
|
|
2632
|
+
const missing = [];
|
|
2633
|
+
const localDefinitions = /* @__PURE__ */ new Set();
|
|
2634
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2635
|
+
const line = lines[i];
|
|
2636
|
+
const classMatch = line.match(/^class\s+([A-Z][a-zA-Z0-9_]*)/);
|
|
2637
|
+
if (classMatch) {
|
|
2638
|
+
localDefinitions.add(classMatch[1]);
|
|
2639
|
+
}
|
|
2640
|
+
const funcMatch = line.match(/^(?:async\s+)?def\s+([A-Z][a-zA-Z0-9_]*)/);
|
|
2641
|
+
if (funcMatch) {
|
|
2642
|
+
localDefinitions.add(funcMatch[1]);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
const potentialImports = /* @__PURE__ */ new Set();
|
|
2646
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2647
|
+
let line = lines[i];
|
|
2648
|
+
if (line.trim().startsWith("import ") || line.trim().startsWith("from ") || line.trim().startsWith("#")) {
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
2651
|
+
line = line.replace(/#.*$/g, "");
|
|
2652
|
+
line = line.replace(/[`"'f].*?[`"']/g, "");
|
|
2653
|
+
const matches = line.matchAll(/(?<!\.)([A-Z][a-zA-Z0-9_]*)(?!\.)/g);
|
|
2654
|
+
for (const match of matches) {
|
|
2655
|
+
const identifier = match[1];
|
|
2656
|
+
if (!isPythonBuiltin(identifier)) {
|
|
2657
|
+
potentialImports.add(identifier);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
for (const identifier of potentialImports) {
|
|
2662
|
+
if (!imports.has(identifier) && !localDefinitions.has(identifier)) {
|
|
2663
|
+
const usageLines = [];
|
|
2664
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2665
|
+
const line = lines[i];
|
|
2666
|
+
const regex = new RegExp(`\\b${identifier}\\b`);
|
|
2667
|
+
if (regex.test(line) && !line.trim().startsWith("#")) {
|
|
2668
|
+
usageLines.push(i + 1);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
if (usageLines.length > 0) {
|
|
2672
|
+
missing.push({
|
|
2673
|
+
identifier,
|
|
2674
|
+
line: usageLines[0],
|
|
2675
|
+
type: "missing",
|
|
2676
|
+
usedAt: usageLines
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
return { missing, unused };
|
|
1488
2682
|
}
|
|
1489
2683
|
function analyzeImports(filePath) {
|
|
1490
2684
|
if (filePath.endsWith(".py")) {
|
|
@@ -1496,14 +2690,14 @@ function analyzeImports(filePath) {
|
|
|
1496
2690
|
}
|
|
1497
2691
|
|
|
1498
2692
|
// src/analysis/docker.ts
|
|
1499
|
-
import { readFileSync as
|
|
2693
|
+
import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
|
|
1500
2694
|
import { load as parseYaml } from "js-yaml";
|
|
1501
2695
|
import { join as join2 } from "path";
|
|
1502
2696
|
function parseDockerfile(dockerfilePath) {
|
|
1503
2697
|
if (!existsSync2(dockerfilePath)) {
|
|
1504
2698
|
return null;
|
|
1505
2699
|
}
|
|
1506
|
-
const content =
|
|
2700
|
+
const content = readFileSync4(dockerfilePath, "utf-8");
|
|
1507
2701
|
const lines = content.split("\n");
|
|
1508
2702
|
let inHealthCheck = false;
|
|
1509
2703
|
for (const line of lines) {
|
|
@@ -1581,7 +2775,7 @@ function parseDockerCompose(projectRoot) {
|
|
|
1581
2775
|
let composeData = null;
|
|
1582
2776
|
for (const file of composeFiles) {
|
|
1583
2777
|
if (existsSync2(file)) {
|
|
1584
|
-
const content =
|
|
2778
|
+
const content = readFileSync4(file, "utf-8");
|
|
1585
2779
|
composeData = parseYaml(content);
|
|
1586
2780
|
break;
|
|
1587
2781
|
}
|
|
@@ -1650,6 +2844,549 @@ function calculateServiceBlastRadius(graph, serviceName) {
|
|
|
1650
2844
|
return Array.from(affected);
|
|
1651
2845
|
}
|
|
1652
2846
|
|
|
2847
|
+
// src/analysis/line-diff.ts
|
|
2848
|
+
import { execSync as execSync2 } from "child_process";
|
|
2849
|
+
function parseLineLevelDiff(projectRoot) {
|
|
2850
|
+
try {
|
|
2851
|
+
const diffOutput = execSync2("git diff HEAD", {
|
|
2852
|
+
cwd: projectRoot,
|
|
2853
|
+
encoding: "utf-8",
|
|
2854
|
+
maxBuffer: 10 * 1024 * 1024
|
|
2855
|
+
// 10MB buffer
|
|
2856
|
+
});
|
|
2857
|
+
if (!diffOutput.trim()) {
|
|
2858
|
+
return {
|
|
2859
|
+
removedLines: [],
|
|
2860
|
+
addedLines: [],
|
|
2861
|
+
modifiedLines: []
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
return parseDiffOutput(diffOutput);
|
|
2865
|
+
} catch (error) {
|
|
2866
|
+
return {
|
|
2867
|
+
removedLines: [],
|
|
2868
|
+
addedLines: [],
|
|
2869
|
+
modifiedLines: []
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
function parseDiffOutput(diffOutput) {
|
|
2874
|
+
const removedLines = [];
|
|
2875
|
+
const addedLines = [];
|
|
2876
|
+
let currentFile = "";
|
|
2877
|
+
let currentFunction = "";
|
|
2878
|
+
let removedLineNumber = 0;
|
|
2879
|
+
let addedLineNumber = 0;
|
|
2880
|
+
const lines = diffOutput.split("\n");
|
|
2881
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2882
|
+
const line = lines[i];
|
|
2883
|
+
if (line.startsWith("diff --git")) {
|
|
2884
|
+
const match = line.match(/b\/(.*?)$/);
|
|
2885
|
+
if (match) {
|
|
2886
|
+
currentFile = match[1].trim();
|
|
2887
|
+
}
|
|
2888
|
+
continue;
|
|
2889
|
+
}
|
|
2890
|
+
if (line.startsWith("@@")) {
|
|
2891
|
+
const match = line.match(/@@ -(\d+),\d+ \+(\d+),\d+ @@(.*)/);
|
|
2892
|
+
if (match) {
|
|
2893
|
+
removedLineNumber = parseInt(match[1]);
|
|
2894
|
+
addedLineNumber = parseInt(match[2]);
|
|
2895
|
+
const context = match[3].trim();
|
|
2896
|
+
if (context) {
|
|
2897
|
+
currentFunction = extractFunctionName(context);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
continue;
|
|
2901
|
+
}
|
|
2902
|
+
if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("index ")) {
|
|
2903
|
+
continue;
|
|
2904
|
+
}
|
|
2905
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
2906
|
+
const content = line.substring(1);
|
|
2907
|
+
if (content.trim()) {
|
|
2908
|
+
removedLines.push({
|
|
2909
|
+
file: currentFile,
|
|
2910
|
+
lineNumber: removedLineNumber,
|
|
2911
|
+
content,
|
|
2912
|
+
inFunction: currentFunction || void 0
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
removedLineNumber++;
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2918
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
2919
|
+
const content = line.substring(1);
|
|
2920
|
+
if (content.trim()) {
|
|
2921
|
+
addedLines.push({
|
|
2922
|
+
file: currentFile,
|
|
2923
|
+
lineNumber: addedLineNumber,
|
|
2924
|
+
content,
|
|
2925
|
+
inFunction: currentFunction || void 0
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
addedLineNumber++;
|
|
2929
|
+
continue;
|
|
2930
|
+
}
|
|
2931
|
+
if (line.startsWith(" ") || !line.startsWith("-") && !line.startsWith("+")) {
|
|
2932
|
+
removedLineNumber++;
|
|
2933
|
+
addedLineNumber++;
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
const modifiedLines = matchModifiedLines(removedLines, addedLines);
|
|
2937
|
+
return {
|
|
2938
|
+
removedLines,
|
|
2939
|
+
addedLines,
|
|
2940
|
+
modifiedLines
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
function extractFunctionName(context) {
|
|
2944
|
+
context = context.replace(/^(function|def|class|const|let|var|async|export)\s+/, "");
|
|
2945
|
+
const patterns = [
|
|
2946
|
+
/^(\w+)\s*\(/,
|
|
2947
|
+
// functionName(
|
|
2948
|
+
/^(\w+)\s*=/,
|
|
2949
|
+
// functionName =
|
|
2950
|
+
/^(\w+)\s*{/,
|
|
2951
|
+
// ClassName {
|
|
2952
|
+
/^(\w+)\s*:/
|
|
2953
|
+
// functionName:
|
|
2954
|
+
];
|
|
2955
|
+
for (const pattern of patterns) {
|
|
2956
|
+
const match = context.match(pattern);
|
|
2957
|
+
if (match) {
|
|
2958
|
+
return match[1];
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
const firstWord = context.split(/\s+/)[0];
|
|
2962
|
+
return firstWord || "";
|
|
2963
|
+
}
|
|
2964
|
+
function matchModifiedLines(removedLines, addedLines) {
|
|
2965
|
+
const modifiedLines = [];
|
|
2966
|
+
const removedByLocation = /* @__PURE__ */ new Map();
|
|
2967
|
+
const addedByLocation = /* @__PURE__ */ new Map();
|
|
2968
|
+
for (const removed of removedLines) {
|
|
2969
|
+
const key = `${removed.file}:${removed.inFunction || "global"}`;
|
|
2970
|
+
if (!removedByLocation.has(key)) {
|
|
2971
|
+
removedByLocation.set(key, []);
|
|
2972
|
+
}
|
|
2973
|
+
removedByLocation.get(key).push(removed);
|
|
2974
|
+
}
|
|
2975
|
+
for (const added of addedLines) {
|
|
2976
|
+
const key = `${added.file}:${added.inFunction || "global"}`;
|
|
2977
|
+
if (!addedByLocation.has(key)) {
|
|
2978
|
+
addedByLocation.set(key, []);
|
|
2979
|
+
}
|
|
2980
|
+
addedByLocation.get(key).push(added);
|
|
2981
|
+
}
|
|
2982
|
+
for (const [location, removedLinesInLocation] of removedByLocation) {
|
|
2983
|
+
const addedLinesInLocation = addedByLocation.get(location) || [];
|
|
2984
|
+
for (const removed of removedLinesInLocation) {
|
|
2985
|
+
const matchingAdded = addedLinesInLocation.find(
|
|
2986
|
+
(added) => Math.abs(added.lineNumber - removed.lineNumber) <= 2
|
|
2987
|
+
);
|
|
2988
|
+
if (matchingAdded) {
|
|
2989
|
+
modifiedLines.push({
|
|
2990
|
+
before: removed,
|
|
2991
|
+
after: matchingAdded
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
return modifiedLines;
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
// src/analysis/removed-calls.ts
|
|
3000
|
+
function extractRemovedCalls(removedLines) {
|
|
3001
|
+
const removedCalls = [];
|
|
3002
|
+
for (const line of removedLines) {
|
|
3003
|
+
const calls = parseLineForCalls(line.content, line.file);
|
|
3004
|
+
for (const call of calls) {
|
|
3005
|
+
removedCalls.push({
|
|
3006
|
+
callee: call.name,
|
|
3007
|
+
removedFrom: line.inFunction || "unknown",
|
|
3008
|
+
file: line.file,
|
|
3009
|
+
lineNumber: line.lineNumber,
|
|
3010
|
+
rawContent: line.content.trim()
|
|
3011
|
+
});
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
return removedCalls;
|
|
3015
|
+
}
|
|
3016
|
+
function parseLineForCalls(lineContent, filePath) {
|
|
3017
|
+
const calls = [];
|
|
3018
|
+
if (isCommentOrString(lineContent)) {
|
|
3019
|
+
return calls;
|
|
3020
|
+
}
|
|
3021
|
+
lineContent = lineContent.replace(/\/\/.*$/g, "");
|
|
3022
|
+
lineContent = lineContent.replace(/#.*$/g, "");
|
|
3023
|
+
lineContent = lineContent.replace(/\/\*.*?\*\//g, "");
|
|
3024
|
+
lineContent = lineContent.replace(/["'`].*?["'`]/g, "");
|
|
3025
|
+
const isJS = /\.(js|ts|jsx|tsx)$/.test(filePath);
|
|
3026
|
+
const isPython = /\.py$/.test(filePath);
|
|
3027
|
+
if (isJS) {
|
|
3028
|
+
calls.push(...extractJavaScriptCalls(lineContent));
|
|
3029
|
+
}
|
|
3030
|
+
if (isPython) {
|
|
3031
|
+
calls.push(...extractPythonCalls(lineContent));
|
|
3032
|
+
}
|
|
3033
|
+
return calls;
|
|
3034
|
+
}
|
|
3035
|
+
function isCommentOrString(line) {
|
|
3036
|
+
const trimmed = line.trim();
|
|
3037
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("#")) {
|
|
3038
|
+
return true;
|
|
3039
|
+
}
|
|
3040
|
+
const stringPrefixes = ['"', "'", "`"];
|
|
3041
|
+
if (stringPrefixes.some((prefix) => trimmed.startsWith(prefix))) {
|
|
3042
|
+
return true;
|
|
3043
|
+
}
|
|
3044
|
+
return false;
|
|
3045
|
+
}
|
|
3046
|
+
function extractJavaScriptCalls(lineContent) {
|
|
3047
|
+
const calls = [];
|
|
3048
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3049
|
+
const simplePattern = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
3050
|
+
let match;
|
|
3051
|
+
while ((match = simplePattern.exec(lineContent)) !== null) {
|
|
3052
|
+
const functionName = match[1];
|
|
3053
|
+
if (!isJavaScriptKeyword(functionName) && !seen.has(functionName)) {
|
|
3054
|
+
calls.push({ name: functionName });
|
|
3055
|
+
seen.add(functionName);
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
const methodPattern = /\.([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
3059
|
+
while ((match = methodPattern.exec(lineContent)) !== null) {
|
|
3060
|
+
const methodName = match[1];
|
|
3061
|
+
if (!isJavaScriptKeyword(methodName) && !seen.has(methodName)) {
|
|
3062
|
+
calls.push({ name: methodName });
|
|
3063
|
+
seen.add(methodName);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
const awaitPattern = /await\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g;
|
|
3067
|
+
while ((match = awaitPattern.exec(lineContent)) !== null) {
|
|
3068
|
+
const functionName = match[1];
|
|
3069
|
+
if (!isJavaScriptKeyword(functionName) && !seen.has(functionName)) {
|
|
3070
|
+
calls.push({ name: functionName });
|
|
3071
|
+
seen.add(functionName);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
return calls;
|
|
3075
|
+
}
|
|
3076
|
+
function extractPythonCalls(lineContent) {
|
|
3077
|
+
const calls = [];
|
|
3078
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3079
|
+
const simplePattern = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
|
|
3080
|
+
let match;
|
|
3081
|
+
while ((match = simplePattern.exec(lineContent)) !== null) {
|
|
3082
|
+
const functionName = match[1];
|
|
3083
|
+
if (!isPythonKeyword(functionName) && !seen.has(functionName)) {
|
|
3084
|
+
calls.push({ name: functionName });
|
|
3085
|
+
seen.add(functionName);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
const methodPattern = /\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
|
|
3089
|
+
while ((match = methodPattern.exec(lineContent)) !== null) {
|
|
3090
|
+
const methodName = match[1];
|
|
3091
|
+
if (!isPythonKeyword(methodName) && !seen.has(methodName)) {
|
|
3092
|
+
calls.push({ name: methodName });
|
|
3093
|
+
seen.add(methodName);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
const awaitPattern = /await\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/g;
|
|
3097
|
+
while ((match = awaitPattern.exec(lineContent)) !== null) {
|
|
3098
|
+
const functionName = match[1];
|
|
3099
|
+
if (!isPythonKeyword(functionName) && !seen.has(functionName)) {
|
|
3100
|
+
calls.push({ name: functionName });
|
|
3101
|
+
seen.add(functionName);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
return calls;
|
|
3105
|
+
}
|
|
3106
|
+
function isJavaScriptKeyword(identifier) {
|
|
3107
|
+
const keywords = /* @__PURE__ */ new Set([
|
|
3108
|
+
// Keywords
|
|
3109
|
+
"if",
|
|
3110
|
+
"else",
|
|
3111
|
+
"for",
|
|
3112
|
+
"while",
|
|
3113
|
+
"do",
|
|
3114
|
+
"switch",
|
|
3115
|
+
"case",
|
|
3116
|
+
"break",
|
|
3117
|
+
"continue",
|
|
3118
|
+
"return",
|
|
3119
|
+
"throw",
|
|
3120
|
+
"try",
|
|
3121
|
+
"catch",
|
|
3122
|
+
"finally",
|
|
3123
|
+
"new",
|
|
3124
|
+
"typeof",
|
|
3125
|
+
"instanceof",
|
|
3126
|
+
"delete",
|
|
3127
|
+
"void",
|
|
3128
|
+
"async",
|
|
3129
|
+
"await",
|
|
3130
|
+
"yield",
|
|
3131
|
+
"import",
|
|
3132
|
+
"export",
|
|
3133
|
+
"from",
|
|
3134
|
+
"class",
|
|
3135
|
+
"extends",
|
|
3136
|
+
"super",
|
|
3137
|
+
"this",
|
|
3138
|
+
"static",
|
|
3139
|
+
"const",
|
|
3140
|
+
"let",
|
|
3141
|
+
"var",
|
|
3142
|
+
"function",
|
|
3143
|
+
"constructor",
|
|
3144
|
+
"get",
|
|
3145
|
+
"set",
|
|
3146
|
+
// Common builtins to skip
|
|
3147
|
+
"console",
|
|
3148
|
+
"parseInt",
|
|
3149
|
+
"parseFloat",
|
|
3150
|
+
"isNaN",
|
|
3151
|
+
"isFinite",
|
|
3152
|
+
"setTimeout",
|
|
3153
|
+
"setInterval",
|
|
3154
|
+
"clearTimeout",
|
|
3155
|
+
"clearInterval",
|
|
3156
|
+
"require",
|
|
3157
|
+
"module",
|
|
3158
|
+
"exports",
|
|
3159
|
+
"__dirname",
|
|
3160
|
+
"__filename"
|
|
3161
|
+
]);
|
|
3162
|
+
return keywords.has(identifier);
|
|
3163
|
+
}
|
|
3164
|
+
function isPythonKeyword(identifier) {
|
|
3165
|
+
const keywords = /* @__PURE__ */ new Set([
|
|
3166
|
+
// Keywords
|
|
3167
|
+
"if",
|
|
3168
|
+
"elif",
|
|
3169
|
+
"else",
|
|
3170
|
+
"for",
|
|
3171
|
+
"while",
|
|
3172
|
+
"break",
|
|
3173
|
+
"continue",
|
|
3174
|
+
"pass",
|
|
3175
|
+
"return",
|
|
3176
|
+
"raise",
|
|
3177
|
+
"try",
|
|
3178
|
+
"except",
|
|
3179
|
+
"finally",
|
|
3180
|
+
"with",
|
|
3181
|
+
"as",
|
|
3182
|
+
"yield",
|
|
3183
|
+
"import",
|
|
3184
|
+
"from",
|
|
3185
|
+
"class",
|
|
3186
|
+
"def",
|
|
3187
|
+
"lambda",
|
|
3188
|
+
"global",
|
|
3189
|
+
"nonlocal",
|
|
3190
|
+
"assert",
|
|
3191
|
+
"del",
|
|
3192
|
+
"in",
|
|
3193
|
+
"is",
|
|
3194
|
+
"not",
|
|
3195
|
+
"and",
|
|
3196
|
+
"or",
|
|
3197
|
+
"async",
|
|
3198
|
+
"await",
|
|
3199
|
+
// Common builtins to skip
|
|
3200
|
+
"print",
|
|
3201
|
+
"len",
|
|
3202
|
+
"range",
|
|
3203
|
+
"enumerate",
|
|
3204
|
+
"zip",
|
|
3205
|
+
"map",
|
|
3206
|
+
"filter",
|
|
3207
|
+
"sorted",
|
|
3208
|
+
"reversed",
|
|
3209
|
+
"sum",
|
|
3210
|
+
"min",
|
|
3211
|
+
"max",
|
|
3212
|
+
"abs",
|
|
3213
|
+
"round",
|
|
3214
|
+
"open",
|
|
3215
|
+
"input",
|
|
3216
|
+
"type",
|
|
3217
|
+
"isinstance",
|
|
3218
|
+
"hasattr",
|
|
3219
|
+
"getattr",
|
|
3220
|
+
"setattr"
|
|
3221
|
+
]);
|
|
3222
|
+
return keywords.has(identifier);
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
// src/analysis/removed-call-impact.ts
|
|
3226
|
+
var SECURITY_KEYWORDS = [
|
|
3227
|
+
// Authentication & Authorization
|
|
3228
|
+
"authenticate",
|
|
3229
|
+
"authorization",
|
|
3230
|
+
"authorize",
|
|
3231
|
+
"login",
|
|
3232
|
+
"logout",
|
|
3233
|
+
"signin",
|
|
3234
|
+
"signout",
|
|
3235
|
+
"verify",
|
|
3236
|
+
"check",
|
|
3237
|
+
"validate",
|
|
3238
|
+
"permission",
|
|
3239
|
+
"access",
|
|
3240
|
+
"role",
|
|
3241
|
+
"admin",
|
|
3242
|
+
"auth",
|
|
3243
|
+
// Input validation & sanitization
|
|
3244
|
+
"sanitize",
|
|
3245
|
+
"escape",
|
|
3246
|
+
"clean",
|
|
3247
|
+
"filter",
|
|
3248
|
+
"whitelist",
|
|
3249
|
+
"blacklist",
|
|
3250
|
+
"validate",
|
|
3251
|
+
"verification",
|
|
3252
|
+
"check",
|
|
3253
|
+
// Security tokens & sessions
|
|
3254
|
+
"token",
|
|
3255
|
+
"session",
|
|
3256
|
+
"cookie",
|
|
3257
|
+
"csrf",
|
|
3258
|
+
"xsrf",
|
|
3259
|
+
"jwt",
|
|
3260
|
+
"oauth",
|
|
3261
|
+
"api_key",
|
|
3262
|
+
"apikey",
|
|
3263
|
+
"secret",
|
|
3264
|
+
"password",
|
|
3265
|
+
"hash",
|
|
3266
|
+
"encrypt",
|
|
3267
|
+
// Injection prevention
|
|
3268
|
+
"xss",
|
|
3269
|
+
"sql",
|
|
3270
|
+
"injection",
|
|
3271
|
+
"query",
|
|
3272
|
+
"prepared",
|
|
3273
|
+
"statement",
|
|
3274
|
+
// Rate limiting & abuse prevention
|
|
3275
|
+
"ratelimit",
|
|
3276
|
+
"throttle",
|
|
3277
|
+
"captcha",
|
|
3278
|
+
"honeypot"
|
|
3279
|
+
];
|
|
3280
|
+
function analyzeRemovedCallImpact(removedCalls, storage) {
|
|
3281
|
+
const impacts = [];
|
|
3282
|
+
for (const removedCall of removedCalls) {
|
|
3283
|
+
try {
|
|
3284
|
+
const impact = analyzeSingleRemovedCall(removedCall, storage);
|
|
3285
|
+
if (impact) {
|
|
3286
|
+
impacts.push(impact);
|
|
3287
|
+
}
|
|
3288
|
+
} catch (error) {
|
|
3289
|
+
console.error(`Error analyzing removed call ${removedCall.callee}:`, error);
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
return impacts;
|
|
3293
|
+
}
|
|
3294
|
+
function analyzeSingleRemovedCall(removedCall, storage) {
|
|
3295
|
+
const nodes = storage.searchNodesByName(removedCall.callee);
|
|
3296
|
+
if (nodes.length === 0) {
|
|
3297
|
+
return null;
|
|
3298
|
+
}
|
|
3299
|
+
const functionNode = nodes[0];
|
|
3300
|
+
const allCallers = storage.getResolvedCallers(functionNode.id);
|
|
3301
|
+
const stillCalledBy = allCallers.filter((c) => c.name !== removedCall.removedFrom).map((c) => ({
|
|
3302
|
+
name: c.name,
|
|
3303
|
+
file: c.filePath,
|
|
3304
|
+
line: c.startLine
|
|
3305
|
+
}));
|
|
3306
|
+
const removedFromNodes = storage.searchNodesByName(removedCall.removedFrom);
|
|
3307
|
+
let impactedFunctions = [];
|
|
3308
|
+
if (removedFromNodes.length > 0) {
|
|
3309
|
+
const removedFromNode = removedFromNodes[0];
|
|
3310
|
+
const impactedNodes = storage.getImpactedNodes(removedFromNode.id, 5);
|
|
3311
|
+
impactedFunctions = impactedNodes.map((n) => ({
|
|
3312
|
+
name: n.name,
|
|
3313
|
+
file: n.filePath,
|
|
3314
|
+
line: n.startLine,
|
|
3315
|
+
reason: `Calls ${removedCall.removedFrom}() which no longer calls ${removedCall.callee}()`
|
|
3316
|
+
}));
|
|
3317
|
+
}
|
|
3318
|
+
const severity = calculateSeverity(
|
|
3319
|
+
removedCall,
|
|
3320
|
+
stillCalledBy,
|
|
3321
|
+
impactedFunctions
|
|
3322
|
+
);
|
|
3323
|
+
const description = buildImpactDescription(
|
|
3324
|
+
removedCall,
|
|
3325
|
+
stillCalledBy,
|
|
3326
|
+
impactedFunctions,
|
|
3327
|
+
severity
|
|
3328
|
+
);
|
|
3329
|
+
return {
|
|
3330
|
+
removedCall,
|
|
3331
|
+
functionNode: {
|
|
3332
|
+
id: functionNode.id,
|
|
3333
|
+
name: functionNode.name,
|
|
3334
|
+
file: functionNode.filePath,
|
|
3335
|
+
line: functionNode.startLine
|
|
3336
|
+
},
|
|
3337
|
+
stillCalledBy,
|
|
3338
|
+
impactedFunctions,
|
|
3339
|
+
severity,
|
|
3340
|
+
description
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
function calculateSeverity(removedCall, stillCalledBy, impactedFunctions) {
|
|
3344
|
+
const isSecurityFunction = SECURITY_KEYWORDS.some(
|
|
3345
|
+
(keyword) => removedCall.callee.toLowerCase().includes(keyword)
|
|
3346
|
+
);
|
|
3347
|
+
if (isSecurityFunction && impactedFunctions.length > 5) {
|
|
3348
|
+
return "CRITICAL";
|
|
3349
|
+
}
|
|
3350
|
+
if (isSecurityFunction && stillCalledBy.length === 0) {
|
|
3351
|
+
return "CRITICAL";
|
|
3352
|
+
}
|
|
3353
|
+
if (stillCalledBy.length === 0) {
|
|
3354
|
+
return "HIGH";
|
|
3355
|
+
}
|
|
3356
|
+
if (impactedFunctions.length > 20) {
|
|
3357
|
+
return "HIGH";
|
|
3358
|
+
}
|
|
3359
|
+
if (impactedFunctions.length >= 10) {
|
|
3360
|
+
return "MEDIUM";
|
|
3361
|
+
}
|
|
3362
|
+
if (isSecurityFunction && impactedFunctions.length > 0) {
|
|
3363
|
+
return "MEDIUM";
|
|
3364
|
+
}
|
|
3365
|
+
return "LOW";
|
|
3366
|
+
}
|
|
3367
|
+
function buildImpactDescription(removedCall, stillCalledBy, impactedFunctions, severity) {
|
|
3368
|
+
const parts = [];
|
|
3369
|
+
parts.push(
|
|
3370
|
+
`${removedCall.callee}() was removed from ${removedCall.removedFrom}() at line ${removedCall.lineNumber}`
|
|
3371
|
+
);
|
|
3372
|
+
if (stillCalledBy.length === 0) {
|
|
3373
|
+
parts.push(`\u26A0\uFE0F ORPHANED: ${removedCall.callee}() is no longer called anywhere in the codebase`);
|
|
3374
|
+
} else {
|
|
3375
|
+
const callerNames = stillCalledBy.slice(0, 3).map((c) => c.name).join(", ");
|
|
3376
|
+
const moreCount = stillCalledBy.length > 3 ? ` and ${stillCalledBy.length - 3} more` : "";
|
|
3377
|
+
parts.push(`Still called by: ${callerNames}${moreCount}`);
|
|
3378
|
+
}
|
|
3379
|
+
if (impactedFunctions.length > 0) {
|
|
3380
|
+
parts.push(
|
|
3381
|
+
`Impact: ${impactedFunctions.length} function${impactedFunctions.length === 1 ? "" : "s"} depend on ${removedCall.removedFrom}()`
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
if (severity === "CRITICAL") {
|
|
3385
|
+
parts.push("\u{1F534} CRITICAL: Security or validation function removed from execution path");
|
|
3386
|
+
}
|
|
3387
|
+
return parts.join("\n");
|
|
3388
|
+
}
|
|
3389
|
+
|
|
1653
3390
|
// src/commands/impact.ts
|
|
1654
3391
|
async function analyzeImpact(options = {}) {
|
|
1655
3392
|
const rootPath = resolve2(options.path || ".");
|
|
@@ -1674,6 +3411,8 @@ async function analyzeImpactWithGit(storage, rootPath, options) {
|
|
|
1674
3411
|
directCallers: /* @__PURE__ */ new Map(),
|
|
1675
3412
|
affectedEndpoints: [],
|
|
1676
3413
|
importIssues: /* @__PURE__ */ new Map(),
|
|
3414
|
+
removedCallImpacts: [],
|
|
3415
|
+
// NEW
|
|
1677
3416
|
riskLevel: "LOW",
|
|
1678
3417
|
totalImpact: 0
|
|
1679
3418
|
};
|
|
@@ -1730,17 +3469,22 @@ async function analyzeImpactWithGit(storage, rootPath, options) {
|
|
|
1730
3469
|
entryPointFiles
|
|
1731
3470
|
};
|
|
1732
3471
|
}
|
|
3472
|
+
const lineDiff = parseLineLevelDiff(rootPath);
|
|
3473
|
+
const removedCalls = extractRemovedCalls(lineDiff.removedLines);
|
|
3474
|
+
const removedCallImpacts = analyzeRemovedCallImpact(removedCalls, storage);
|
|
1733
3475
|
const totalCallers = Array.from(directCallers.values()).reduce((sum, callers) => sum + callers.length, 0);
|
|
1734
3476
|
const hasCriticalImportIssues = Array.from(importIssues.values()).some((i) => i.missing.length > 0);
|
|
1735
3477
|
const hasDockerImpact = dockerImpact && dockerImpact.affectedServices.length > 0;
|
|
3478
|
+
const hasCriticalRemovedCalls = removedCallImpacts.some((i) => i.severity === "CRITICAL");
|
|
3479
|
+
const hasHighRiskRemovedCalls = removedCallImpacts.some((i) => i.severity === "HIGH");
|
|
1736
3480
|
let riskLevel = "LOW";
|
|
1737
|
-
if (hasCriticalImportIssues && hasDockerImpact) {
|
|
3481
|
+
if (hasCriticalRemovedCalls || hasCriticalImportIssues && hasDockerImpact) {
|
|
1738
3482
|
riskLevel = "CRITICAL";
|
|
1739
|
-
} else if (hasCriticalImportIssues || hasDockerImpact && dockerImpact.affectedServices.length > 2) {
|
|
3483
|
+
} else if (hasHighRiskRemovedCalls || hasCriticalImportIssues || hasDockerImpact && dockerImpact.affectedServices.length > 2) {
|
|
1740
3484
|
riskLevel = "HIGH";
|
|
1741
3485
|
} else if (totalCallers > 20 || affectedEndpoints.length > 5) {
|
|
1742
3486
|
riskLevel = "HIGH";
|
|
1743
|
-
} else if (totalCallers > 10 || affectedEndpoints.length > 2 || hasDockerImpact) {
|
|
3487
|
+
} else if (totalCallers > 10 || affectedEndpoints.length > 2 || hasDockerImpact || removedCallImpacts.length > 0) {
|
|
1744
3488
|
riskLevel = "MEDIUM";
|
|
1745
3489
|
}
|
|
1746
3490
|
return {
|
|
@@ -1750,8 +3494,10 @@ async function analyzeImpactWithGit(storage, rootPath, options) {
|
|
|
1750
3494
|
affectedEndpoints,
|
|
1751
3495
|
importIssues,
|
|
1752
3496
|
dockerImpact,
|
|
3497
|
+
removedCallImpacts,
|
|
3498
|
+
// NEW
|
|
1753
3499
|
riskLevel,
|
|
1754
|
-
totalImpact: totalCallers + (dockerImpact?.affectedServices.length || 0)
|
|
3500
|
+
totalImpact: totalCallers + (dockerImpact?.affectedServices.length || 0) + removedCallImpacts.length
|
|
1755
3501
|
};
|
|
1756
3502
|
}
|
|
1757
3503
|
async function analyzeImpactWithHash(storage, rootPath, options) {
|
|
@@ -1786,6 +3532,8 @@ async function analyzeImpactWithHash(storage, rootPath, options) {
|
|
|
1786
3532
|
directCallers: /* @__PURE__ */ new Map(),
|
|
1787
3533
|
affectedEndpoints: [],
|
|
1788
3534
|
importIssues,
|
|
3535
|
+
removedCallImpacts: [],
|
|
3536
|
+
// NEW - not available in hash mode
|
|
1789
3537
|
riskLevel: hasCriticalImportIssues ? "CRITICAL" : changedFiles.length > 10 ? "HIGH" : changedFiles.length > 5 ? "MEDIUM" : "LOW",
|
|
1790
3538
|
totalImpact: result.indexed
|
|
1791
3539
|
};
|
|
@@ -1799,7 +3547,7 @@ function configureMCPServer(projectPath) {
|
|
|
1799
3547
|
if (!existsSync3(claudeConfigPath)) {
|
|
1800
3548
|
return false;
|
|
1801
3549
|
}
|
|
1802
|
-
const config = JSON.parse(
|
|
3550
|
+
const config = JSON.parse(readFileSync5(claudeConfigPath, "utf-8"));
|
|
1803
3551
|
if (!config.mcpServers) {
|
|
1804
3552
|
config.mcpServers = {};
|
|
1805
3553
|
}
|
|
@@ -1813,7 +3561,7 @@ function configureMCPServer(projectPath) {
|
|
|
1813
3561
|
return false;
|
|
1814
3562
|
}
|
|
1815
3563
|
}
|
|
1816
|
-
program.name("codemap").description("CodeMap Flow - Call graph analyzer for understanding code impact").version("3.
|
|
3564
|
+
program.name("codemap").description("CodeMap Flow - Call graph analyzer for understanding code impact").version("3.5.0");
|
|
1817
3565
|
program.command("index").description("Build call graph for your codebase").argument("[path]", "Path to the project root", ".").option("-f, --force", "Force reindex all files (ignore cache)").option("--include <patterns...>", "Glob patterns to include").option("--exclude <patterns...>", "Glob patterns to exclude").action(async (path, options) => {
|
|
1818
3566
|
const rootPath = resolve3(path);
|
|
1819
3567
|
const outputDir = join4(rootPath, ".codemap");
|
|
@@ -2021,12 +3769,12 @@ program.command("impact").description("Analyze impact of code changes").argument
|
|
|
2021
3769
|
for (const [file, issues] of result.importIssues.entries()) {
|
|
2022
3770
|
if (issues.missing.length > 0) {
|
|
2023
3771
|
console.log(chalk.red(` \u274C CRITICAL: ${file} - Missing imports still in use:`));
|
|
2024
|
-
for (const missing of issues.missing.slice(0,
|
|
3772
|
+
for (const missing of issues.missing.slice(0, 10)) {
|
|
2025
3773
|
const usageInfo = missing.usedAt ? ` (used at lines: ${missing.usedAt.slice(0, 3).join(", ")}${missing.usedAt.length > 3 ? "..." : ""})` : "";
|
|
2026
3774
|
console.log(chalk.gray(` \u2022 ${missing.identifier}${usageInfo}`));
|
|
2027
3775
|
}
|
|
2028
|
-
if (issues.missing.length >
|
|
2029
|
-
console.log(chalk.gray(` ... and ${issues.missing.length -
|
|
3776
|
+
if (issues.missing.length > 10) {
|
|
3777
|
+
console.log(chalk.gray(` ... and ${issues.missing.length - 10} more`));
|
|
2030
3778
|
}
|
|
2031
3779
|
}
|
|
2032
3780
|
if (issues.unused.length > 0 && options.verbose) {
|
|
@@ -2037,6 +3785,42 @@ program.command("impact").description("Analyze impact of code changes").argument
|
|
|
2037
3785
|
}
|
|
2038
3786
|
}
|
|
2039
3787
|
}
|
|
3788
|
+
if (result.removedCallImpacts && result.removedCallImpacts.length > 0) {
|
|
3789
|
+
console.log(chalk.blue("\n \u{1F6A8} Removed Function Calls:\n"));
|
|
3790
|
+
for (const impact of result.removedCallImpacts) {
|
|
3791
|
+
const severityColors = {
|
|
3792
|
+
"CRITICAL": chalk.red,
|
|
3793
|
+
"HIGH": chalk.red,
|
|
3794
|
+
"MEDIUM": chalk.yellow,
|
|
3795
|
+
"LOW": chalk.gray
|
|
3796
|
+
};
|
|
3797
|
+
const severityColor = severityColors[impact.severity];
|
|
3798
|
+
console.log(severityColor(` ${impact.severity}: ${impact.removedCall.callee}()`));
|
|
3799
|
+
console.log(chalk.gray(` Removed from: ${impact.removedCall.removedFrom}() at line ${impact.removedCall.lineNumber}`));
|
|
3800
|
+
console.log(chalk.gray(` File: ${impact.removedCall.file}`));
|
|
3801
|
+
if (impact.stillCalledBy.length === 0) {
|
|
3802
|
+
console.log(chalk.red(` \u26A0\uFE0F ORPHANED: No longer called anywhere!`));
|
|
3803
|
+
} else {
|
|
3804
|
+
const callerNames = impact.stillCalledBy.slice(0, 3).map((c) => c.name).join(", ");
|
|
3805
|
+
const moreCount = impact.stillCalledBy.length > 3 ? ` and ${impact.stillCalledBy.length - 3} more` : "";
|
|
3806
|
+
console.log(chalk.gray(` Still called by: ${callerNames}${moreCount}`));
|
|
3807
|
+
}
|
|
3808
|
+
if (impact.impactedFunctions.length > 0) {
|
|
3809
|
+
console.log(chalk.gray(` Impact: ${impact.impactedFunctions.length} dependent function${impact.impactedFunctions.length === 1 ? "" : "s"}`));
|
|
3810
|
+
const topImpacted = impact.impactedFunctions.slice(0, 3);
|
|
3811
|
+
for (const func of topImpacted) {
|
|
3812
|
+
console.log(chalk.gray(` \u2022 ${func.name} (${func.file}:${func.line})`));
|
|
3813
|
+
}
|
|
3814
|
+
if (impact.impactedFunctions.length > 3) {
|
|
3815
|
+
console.log(chalk.gray(` ... and ${impact.impactedFunctions.length - 3} more`));
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
if (impact.severity === "CRITICAL") {
|
|
3819
|
+
console.log(chalk.red(` \u{1F534} ${impact.description.split("\n").pop()}`));
|
|
3820
|
+
}
|
|
3821
|
+
console.log();
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
2040
3824
|
if (result.dockerImpact) {
|
|
2041
3825
|
console.log(chalk.blue("\n \u{1F4E6} Docker Service Impact:\n"));
|
|
2042
3826
|
console.log(chalk.red(` \u26A0\uFE0F Entry point file(s) modified - service(s) will not start:`));
|
|
@@ -2076,7 +3860,7 @@ program.command("reindex").description("Force full re-index of codebase").argume
|
|
|
2076
3860
|
program.command("mcp-server").description("Start MCP server for Claude Code integration").option("-p, --path <path>", "Project root path", ".").action(async (options) => {
|
|
2077
3861
|
process.env.CODEMAP_PROJECT_ROOT = resolve3(options.path);
|
|
2078
3862
|
process.env.CODEMAP_DB_PATH = join4(resolve3(options.path), ".codemap", "graph.db");
|
|
2079
|
-
await import("./flow-server-
|
|
3863
|
+
await import("./flow-server-FMPWJSLK.js");
|
|
2080
3864
|
});
|
|
2081
3865
|
program.parse();
|
|
2082
3866
|
//# sourceMappingURL=cli.js.map
|