codeatlas-mcp-server 2.20.2
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/LICENSE +21 -0
- package/README.md +338 -0
- package/dist/index.js +261 -0
- package/dist/index.js.map +1 -0
- package/dist/src/analyzer/parser.js +1072 -0
- package/dist/src/analyzer/parser.js.map +1 -0
- package/dist/src/analyzer/parser.test.js +73 -0
- package/dist/src/analyzer/parser.test.js.map +1 -0
- package/dist/src/analyzer/phpParser.js +147 -0
- package/dist/src/analyzer/phpParser.js.map +1 -0
- package/dist/src/analyzer/pythonParser.js +185 -0
- package/dist/src/analyzer/pythonParser.js.map +1 -0
- package/dist/src/analyzer/types.js +2 -0
- package/dist/src/analyzer/types.js.map +1 -0
- package/dist/src/context.js +3 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/memoryGenerator.js +293 -0
- package/dist/src/memoryGenerator.js.map +1 -0
- package/dist/src/oracleDatabase.js +298 -0
- package/dist/src/oracleDatabase.js.map +1 -0
- package/dist/src/presentation/httpServer.js +306 -0
- package/dist/src/presentation/httpServer.js.map +1 -0
- package/dist/src/presentation/mcpServer.js +1487 -0
- package/dist/src/presentation/mcpServer.js.map +1 -0
- package/dist/src/repositories.js +144 -0
- package/dist/src/repositories.js.map +1 -0
- package/dist/src/securityScanner.js +69 -0
- package/dist/src/securityScanner.js.map +1 -0
- package/dist/src/services/authService.js +24 -0
- package/dist/src/services/authService.js.map +1 -0
- package/dist/src/services/dreamingService.js +119 -0
- package/dist/src/services/dreamingService.js.map +1 -0
- package/dist/src/services/dreamingService.test.js +179 -0
- package/dist/src/services/dreamingService.test.js.map +1 -0
- package/dist/src/services/projectService.js +1068 -0
- package/dist/src/services/projectService.js.map +1 -0
- package/dist/src/services/projectService.test.js +217 -0
- package/dist/src/services/projectService.test.js.map +1 -0
- package/dist/src/services/watcherService.js +164 -0
- package/dist/src/services/watcherService.js.map +1 -0
- package/dist/src/services/watcherService.test.js +65 -0
- package/dist/src/services/watcherService.test.js.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,1072 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { parse } from '@typescript-eslint/typescript-estree';
|
|
4
|
+
import ignore from 'ignore';
|
|
5
|
+
import { PythonParser } from './pythonParser.js';
|
|
6
|
+
import { PhpParser } from './phpParser.js';
|
|
7
|
+
export class CodeAnalyzer {
|
|
8
|
+
workspaceRoot;
|
|
9
|
+
nodes = new Map();
|
|
10
|
+
links = [];
|
|
11
|
+
maxFiles;
|
|
12
|
+
excludedDirectories;
|
|
13
|
+
excludedFiles;
|
|
14
|
+
fileExtensions;
|
|
15
|
+
ignoreFilter = null;
|
|
16
|
+
constructor(workspaceRoot, maxFiles = 5000, excludedDirectories = ['node_modules', 'dist', 'out', '.git', '__pycache__', '.venv', 'venv', 'env', '.env', 'vendor', 'build', '.tox', '.mypy_cache', '.pytest_cache', 'coverage', '.next', '.nuxt',
|
|
17
|
+
// Common user home subdirectories — prevent accidental full-home scans
|
|
18
|
+
'Downloads', 'Desktop', 'Documents', 'Pictures', 'Music', 'Videos', 'snap', 'go', 'AppData', 'Applications', 'Public', 'Templates'], fileExtensions = ['.ts', '.tsx', '.js', '.jsx', '.py', '.php'], excludedFiles = ['_ide_helper.php', '_ide_helper_models.php', '.phpstorm.meta.php']) {
|
|
19
|
+
this.workspaceRoot = workspaceRoot;
|
|
20
|
+
this.maxFiles = maxFiles;
|
|
21
|
+
this.excludedDirectories = excludedDirectories;
|
|
22
|
+
this.excludedFiles = excludedFiles;
|
|
23
|
+
this.fileExtensions = fileExtensions;
|
|
24
|
+
}
|
|
25
|
+
getIgnoreFilter() {
|
|
26
|
+
if (this.ignoreFilter) {
|
|
27
|
+
return this.ignoreFilter;
|
|
28
|
+
}
|
|
29
|
+
const ig = ignore();
|
|
30
|
+
// Excluded dirs/files will match relative paths correctly
|
|
31
|
+
ig.add(this.excludedDirectories);
|
|
32
|
+
ig.add(this.excludedFiles);
|
|
33
|
+
const gitignorePath = path.join(this.workspaceRoot, '.gitignore');
|
|
34
|
+
if (fs.existsSync(gitignorePath)) {
|
|
35
|
+
try {
|
|
36
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
37
|
+
ig.add(gitignoreContent);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
console.warn(`[CodeAnalyzer] Failed to read .gitignore at ${gitignorePath}`, e);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
this.ignoreFilter = ig;
|
|
44
|
+
return ig;
|
|
45
|
+
}
|
|
46
|
+
isIgnored(absolutePath, isDirectory) {
|
|
47
|
+
const relPath = path.relative(this.workspaceRoot, absolutePath);
|
|
48
|
+
if (!relPath || relPath === '.' || relPath.startsWith('..')) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
let normalizedPath = relPath.replace(/\\/g, '/');
|
|
52
|
+
if (isDirectory && !normalizedPath.endsWith('/')) {
|
|
53
|
+
normalizedPath += '/';
|
|
54
|
+
}
|
|
55
|
+
return this.getIgnoreFilter().ignores(normalizedPath);
|
|
56
|
+
}
|
|
57
|
+
allFiles = [];
|
|
58
|
+
totalSkippedCount = 0;
|
|
59
|
+
async analyzeProject(onProgress) {
|
|
60
|
+
this.nodes.clear();
|
|
61
|
+
this.links = [];
|
|
62
|
+
let files = this.getFiles(this.workspaceRoot);
|
|
63
|
+
if (files.length > this.maxFiles) {
|
|
64
|
+
console.warn(`[CodeAnalyzer] Workspace has ${files.length} files, which exceeds maxFiles (${this.maxFiles}). Truncating to ${this.maxFiles} files.`);
|
|
65
|
+
files = files.slice(0, this.maxFiles);
|
|
66
|
+
}
|
|
67
|
+
this.allFiles = [...files];
|
|
68
|
+
const total = files.length;
|
|
69
|
+
// Log the files to be indexed
|
|
70
|
+
const relativeFiles = files.map(f => path.relative(this.workspaceRoot, f));
|
|
71
|
+
console.error(`[Indexing] 📋 Discovered ${relativeFiles.length} files to index:\n` + relativeFiles.map(f => ` - ${f}`).join('\n'));
|
|
72
|
+
let totalSkipped = 0;
|
|
73
|
+
let lastReportedPercent = 0;
|
|
74
|
+
for (let i = 0; i < total; i++) {
|
|
75
|
+
const filePath = files[i];
|
|
76
|
+
const success = this.analyzeFile(filePath);
|
|
77
|
+
if (!success)
|
|
78
|
+
totalSkipped++;
|
|
79
|
+
if (onProgress && total > 0) {
|
|
80
|
+
const percent = Math.floor(((i + 1) / total) * 100);
|
|
81
|
+
const relPath = path.relative(this.workspaceRoot, filePath);
|
|
82
|
+
// Report every 10% milestone to avoid log spam
|
|
83
|
+
if (percent >= lastReportedPercent + 10 || percent === 100) {
|
|
84
|
+
lastReportedPercent = percent;
|
|
85
|
+
onProgress(percent, i + 1, total, relPath);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this.totalSkippedCount = totalSkipped;
|
|
90
|
+
return this.buildAnalysisResult();
|
|
91
|
+
}
|
|
92
|
+
async analyzeFileIncremental(filePath) {
|
|
93
|
+
const absPath = path.resolve(filePath);
|
|
94
|
+
// 1. Remove all existing nodes belonging to this file
|
|
95
|
+
const nodesToRemove = new Set();
|
|
96
|
+
this.nodes.forEach((node, id) => {
|
|
97
|
+
if (node.filePath && path.resolve(this.workspaceRoot, node.filePath) === absPath) {
|
|
98
|
+
nodesToRemove.add(id);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
// Also remove module node matching this file's module ID
|
|
102
|
+
const relativePath = path.relative(this.workspaceRoot, absPath);
|
|
103
|
+
const normalizedRelativePath = relativePath.replace(/\\/g, '/');
|
|
104
|
+
const moduleId = `module:${normalizedRelativePath}`;
|
|
105
|
+
nodesToRemove.add(moduleId);
|
|
106
|
+
nodesToRemove.forEach(id => this.nodes.delete(id));
|
|
107
|
+
// 2. Remove all links associated with the removed nodes
|
|
108
|
+
this.links = this.links.filter(link => !nodesToRemove.has(link.source) && !nodesToRemove.has(link.target));
|
|
109
|
+
// 3. Re-analyze only if file exists and is NOT ignored
|
|
110
|
+
try {
|
|
111
|
+
if (fs.existsSync(absPath) && !this.isIgnored(absPath, false)) {
|
|
112
|
+
this.analyzeFile(absPath);
|
|
113
|
+
if (!this.allFiles.includes(absPath)) {
|
|
114
|
+
this.allFiles.push(absPath);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// File was deleted or is ignored
|
|
119
|
+
this.allFiles = this.allFiles.filter(f => f !== absPath);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
this.allFiles = this.allFiles.filter(f => f !== absPath);
|
|
124
|
+
}
|
|
125
|
+
return this.buildAnalysisResult();
|
|
126
|
+
}
|
|
127
|
+
buildAnalysisResult() {
|
|
128
|
+
// Add graph layout sizes based on relationships
|
|
129
|
+
this.nodes.forEach(node => {
|
|
130
|
+
let degree = this.links.filter(l => l.source === node.id || l.target === node.id).length;
|
|
131
|
+
node.val = (node.type === 'module' ? 8 : (node.type === 'class' ? 6 : 4)) + Math.log1p(degree) * 2;
|
|
132
|
+
});
|
|
133
|
+
// Only keep links where both source and target nodes exist
|
|
134
|
+
const validLinks = this.links.filter(link => this.nodes.has(link.source) && this.nodes.has(link.target));
|
|
135
|
+
const graph = {
|
|
136
|
+
nodes: Array.from(this.nodes.values()),
|
|
137
|
+
links: validLinks
|
|
138
|
+
};
|
|
139
|
+
const insights = this.generateAIInsights(graph);
|
|
140
|
+
const circularDepsCount = this.detectCircularDeps();
|
|
141
|
+
const counts = {
|
|
142
|
+
modules: Array.from(this.nodes.values()).filter(n => n.type === 'module').length,
|
|
143
|
+
functions: Array.from(this.nodes.values()).filter(n => n.type === 'function').length,
|
|
144
|
+
classes: Array.from(this.nodes.values()).filter(n => n.type === 'class').length,
|
|
145
|
+
variables: Array.from(this.nodes.values()).filter(n => n.type === 'variable').length,
|
|
146
|
+
dependencies: this.links.filter(l => l.type === 'import').length,
|
|
147
|
+
circularDeps: circularDepsCount,
|
|
148
|
+
deadCode: 0
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
graph,
|
|
152
|
+
insights,
|
|
153
|
+
entityCounts: counts,
|
|
154
|
+
totalFilesAnalyzed: this.allFiles.length - this.totalSkippedCount,
|
|
155
|
+
totalFilesSkipped: this.totalSkippedCount
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Builds chunked analysis data grouped by folder.
|
|
160
|
+
* Call this after analyzeProject() to get folder-based chunks.
|
|
161
|
+
*/
|
|
162
|
+
buildChunkedResult(result) {
|
|
163
|
+
// Group nodes by their parent folder
|
|
164
|
+
const folderNodeMap = new Map();
|
|
165
|
+
for (const node of result.graph.nodes) {
|
|
166
|
+
let folder = '.'; // root
|
|
167
|
+
if (node.filePath) {
|
|
168
|
+
const rel = path.isAbsolute(node.filePath)
|
|
169
|
+
? path.relative(this.workspaceRoot, node.filePath)
|
|
170
|
+
: node.filePath;
|
|
171
|
+
folder = path.dirname(rel).replace(/\\/g, '/');
|
|
172
|
+
}
|
|
173
|
+
else if (node.id.startsWith('module:')) {
|
|
174
|
+
const rel = node.id.replace('module:', '');
|
|
175
|
+
folder = path.dirname(rel).replace(/\\/g, '/');
|
|
176
|
+
}
|
|
177
|
+
else if (node.id.startsWith('external:')) {
|
|
178
|
+
folder = '__external__';
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// Extract folder from node id (e.g. "class:module:src/foo.ts:MyClass")
|
|
182
|
+
const parts = node.id.split(':');
|
|
183
|
+
if (parts.length >= 3 && parts[1] === 'module') {
|
|
184
|
+
// Reconstruct the path from parts[2] (e.g. "src/foo.ts")
|
|
185
|
+
folder = path.dirname(parts[2]).replace(/\\/g, '/');
|
|
186
|
+
}
|
|
187
|
+
else if (parts.length >= 2) {
|
|
188
|
+
const moduleRef = parts.slice(1, -1).join(':').replace(/^module:/, '');
|
|
189
|
+
if (moduleRef) {
|
|
190
|
+
folder = path.dirname(moduleRef).replace(/\\/g, '/');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!folderNodeMap.has(folder)) {
|
|
195
|
+
folderNodeMap.set(folder, []);
|
|
196
|
+
}
|
|
197
|
+
folderNodeMap.get(folder).push(node);
|
|
198
|
+
}
|
|
199
|
+
// Build chunks and separate cross-chunk links
|
|
200
|
+
const chunks = new Map();
|
|
201
|
+
const nodeToFolder = new Map();
|
|
202
|
+
const crossLinks = [];
|
|
203
|
+
// Map each node to its folder
|
|
204
|
+
for (const [folder, nodes] of folderNodeMap) {
|
|
205
|
+
for (const node of nodes) {
|
|
206
|
+
nodeToFolder.set(node.id, folder);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Create chunks with internal links
|
|
210
|
+
for (const [folder, nodes] of folderNodeMap) {
|
|
211
|
+
const nodeIds = new Set(nodes.map(n => n.id));
|
|
212
|
+
const internalLinks = result.graph.links.filter(link => nodeIds.has(link.source) && nodeIds.has(link.target));
|
|
213
|
+
chunks.set(folder, {
|
|
214
|
+
folderPath: folder,
|
|
215
|
+
nodes,
|
|
216
|
+
links: internalLinks
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
// Collect cross-chunk links
|
|
220
|
+
for (const link of result.graph.links) {
|
|
221
|
+
const srcFolder = nodeToFolder.get(link.source);
|
|
222
|
+
const tgtFolder = nodeToFolder.get(link.target);
|
|
223
|
+
if (srcFolder && tgtFolder && srcFolder !== tgtFolder) {
|
|
224
|
+
crossLinks.push(link);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Build folder info for manifest
|
|
228
|
+
const folders = [];
|
|
229
|
+
for (const [folder, chunk] of chunks) {
|
|
230
|
+
const types = {};
|
|
231
|
+
for (const node of chunk.nodes) {
|
|
232
|
+
types[node.type] = (types[node.type] || 0) + 1;
|
|
233
|
+
}
|
|
234
|
+
folders.push({
|
|
235
|
+
path: folder,
|
|
236
|
+
nodeCount: chunk.nodes.length,
|
|
237
|
+
linkCount: chunk.links.length,
|
|
238
|
+
types
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// Sort folders by node count descending for prioritized loading
|
|
242
|
+
folders.sort((a, b) => b.nodeCount - a.nodeCount);
|
|
243
|
+
const manifest = {
|
|
244
|
+
totalNodes: result.graph.nodes.length,
|
|
245
|
+
totalLinks: result.graph.links.length,
|
|
246
|
+
totalFiles: result.totalFilesAnalyzed + result.totalFilesSkipped,
|
|
247
|
+
totalFilesSkipped: result.totalFilesSkipped,
|
|
248
|
+
folders,
|
|
249
|
+
insights: result.insights,
|
|
250
|
+
entityCounts: result.entityCounts
|
|
251
|
+
};
|
|
252
|
+
return { manifest, chunks, crossChunkLinks: { links: crossLinks } };
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Returns the first N nodes for initial loading, picking from folders in order.
|
|
256
|
+
*/
|
|
257
|
+
getInitialLoadData(result, manifest, chunks, crossChunkLinks, nodeLimit) {
|
|
258
|
+
if (nodeLimit <= 0 || nodeLimit >= result.graph.nodes.length) {
|
|
259
|
+
// Load everything
|
|
260
|
+
return {
|
|
261
|
+
graph: result.graph,
|
|
262
|
+
loadedFolders: manifest.folders.map(f => f.path)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const loadedNodes = [];
|
|
266
|
+
const loadedFolders = [];
|
|
267
|
+
let remaining = nodeLimit;
|
|
268
|
+
// Load folders by priority (sorted by nodeCount descending — most important first)
|
|
269
|
+
// Actually, for better UX, sort by path alphabetically so root folders load first
|
|
270
|
+
const sortedFolders = [...manifest.folders].sort((a, b) => {
|
|
271
|
+
// Root folder first, then by depth, then alphabetically
|
|
272
|
+
if (a.path === '.')
|
|
273
|
+
return -1;
|
|
274
|
+
if (b.path === '.')
|
|
275
|
+
return 1;
|
|
276
|
+
const depthA = a.path.split('/').length;
|
|
277
|
+
const depthB = b.path.split('/').length;
|
|
278
|
+
if (depthA !== depthB)
|
|
279
|
+
return depthA - depthB;
|
|
280
|
+
return a.path.localeCompare(b.path);
|
|
281
|
+
});
|
|
282
|
+
for (const folderInfo of sortedFolders) {
|
|
283
|
+
if (remaining <= 0)
|
|
284
|
+
break;
|
|
285
|
+
const chunk = chunks.get(folderInfo.path);
|
|
286
|
+
if (!chunk)
|
|
287
|
+
continue;
|
|
288
|
+
if (chunk.nodes.length <= remaining) {
|
|
289
|
+
loadedNodes.push(...chunk.nodes);
|
|
290
|
+
loadedFolders.push(folderInfo.path);
|
|
291
|
+
remaining -= chunk.nodes.length;
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Partial load: take module nodes first, then classes, then functions, then variables
|
|
295
|
+
const priorityOrder = ['module', 'class', 'function', 'variable'];
|
|
296
|
+
const sorted = [...chunk.nodes].sort((a, b) => {
|
|
297
|
+
const indexA = priorityOrder.indexOf(a.type);
|
|
298
|
+
const indexB = priorityOrder.indexOf(b.type);
|
|
299
|
+
// Unknown types go to the end
|
|
300
|
+
const priorityA = indexA === -1 ? priorityOrder.length : indexA;
|
|
301
|
+
const priorityB = indexB === -1 ? priorityOrder.length : indexB;
|
|
302
|
+
return priorityA - priorityB;
|
|
303
|
+
});
|
|
304
|
+
loadedNodes.push(...sorted.slice(0, remaining));
|
|
305
|
+
loadedFolders.push(folderInfo.path);
|
|
306
|
+
remaining = 0;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Filter links to only include those where both endpoints are loaded
|
|
310
|
+
const loadedNodeIds = new Set(loadedNodes.map(n => n.id));
|
|
311
|
+
const loadedLinks = result.graph.links.filter(link => loadedNodeIds.has(link.source) && loadedNodeIds.has(link.target));
|
|
312
|
+
return {
|
|
313
|
+
graph: { nodes: loadedNodes, links: loadedLinks },
|
|
314
|
+
loadedFolders
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Detects circular dependencies among modules.
|
|
319
|
+
* Builds a directed graph of module imports and runs DFS cycle detection.
|
|
320
|
+
* @returns {number} The total number of back-edges (cycles) found.
|
|
321
|
+
*/
|
|
322
|
+
detectCircularDeps() {
|
|
323
|
+
const adjList = new Map();
|
|
324
|
+
// Build adjacency list for module-to-module imports
|
|
325
|
+
for (const link of this.links) {
|
|
326
|
+
if (link.type === 'import' && link.source.startsWith('module:') && link.target.startsWith('module:')) {
|
|
327
|
+
if (!adjList.has(link.source)) {
|
|
328
|
+
adjList.set(link.source, []);
|
|
329
|
+
}
|
|
330
|
+
adjList.get(link.source).push(link.target);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
let cycles = 0;
|
|
334
|
+
const visited = new Set();
|
|
335
|
+
const recStack = new Set();
|
|
336
|
+
const dfs = (node) => {
|
|
337
|
+
visited.add(node);
|
|
338
|
+
recStack.add(node);
|
|
339
|
+
const neighbors = adjList.get(node) || [];
|
|
340
|
+
for (const neighbor of neighbors) {
|
|
341
|
+
if (!visited.has(neighbor)) {
|
|
342
|
+
dfs(neighbor);
|
|
343
|
+
}
|
|
344
|
+
else if (recStack.has(neighbor)) {
|
|
345
|
+
cycles++;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
recStack.delete(node);
|
|
349
|
+
};
|
|
350
|
+
for (const node of adjList.keys()) {
|
|
351
|
+
if (!visited.has(node)) {
|
|
352
|
+
dfs(node);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return cycles;
|
|
356
|
+
}
|
|
357
|
+
getFiles(dir, fileList = [], depth = 0) {
|
|
358
|
+
if (!fs.existsSync(dir))
|
|
359
|
+
return fileList;
|
|
360
|
+
// Safety: max recursion depth = 20 levels (prevents runaway on broad paths)
|
|
361
|
+
const MAX_DEPTH = 20;
|
|
362
|
+
if (depth > MAX_DEPTH) {
|
|
363
|
+
console.warn(`[CodeAnalyzer] Max depth (${MAX_DEPTH}) reached at ${dir}. Stopping recursion.`);
|
|
364
|
+
return fileList;
|
|
365
|
+
}
|
|
366
|
+
// Check if current directory itself is ignored
|
|
367
|
+
if (dir !== this.workspaceRoot && this.isIgnored(dir, true)) {
|
|
368
|
+
return fileList;
|
|
369
|
+
}
|
|
370
|
+
let files;
|
|
371
|
+
try {
|
|
372
|
+
files = fs.readdirSync(dir);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return fileList;
|
|
376
|
+
}
|
|
377
|
+
for (const file of files) {
|
|
378
|
+
// Keep safety check for hidden files/folders (starting with .)
|
|
379
|
+
if (file.startsWith('.')) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const filePath = path.join(dir, file);
|
|
383
|
+
let stat;
|
|
384
|
+
try {
|
|
385
|
+
stat = fs.statSync(filePath);
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const isDirectory = stat.isDirectory();
|
|
391
|
+
// Check if file/directory is ignored via .gitignore or default rules
|
|
392
|
+
if (this.isIgnored(filePath, isDirectory)) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (isDirectory) {
|
|
396
|
+
this.getFiles(filePath, fileList, depth + 1);
|
|
397
|
+
}
|
|
398
|
+
else if (this.fileExtensions.some(ext => file.endsWith(ext))) {
|
|
399
|
+
fileList.push(filePath);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return fileList;
|
|
403
|
+
}
|
|
404
|
+
analyzeFile(filePath) {
|
|
405
|
+
let moduleId = '';
|
|
406
|
+
try {
|
|
407
|
+
const code = fs.readFileSync(filePath, 'utf-8');
|
|
408
|
+
const relativePath = path.relative(this.workspaceRoot, filePath);
|
|
409
|
+
// Normalize to use forward slashes for matching
|
|
410
|
+
const normalizedRelativePath = relativePath.replace(/\\/g, '/');
|
|
411
|
+
moduleId = `module:${normalizedRelativePath}`;
|
|
412
|
+
const isPython = filePath.endsWith('.py');
|
|
413
|
+
const isPhp = filePath.endsWith('.php') && !filePath.endsWith('.blade.php');
|
|
414
|
+
const isBlade = filePath.endsWith('.blade.php');
|
|
415
|
+
// Set color based on file type
|
|
416
|
+
let moduleColor = '#4cc9f0'; // TS/JS default
|
|
417
|
+
if (isPython)
|
|
418
|
+
moduleColor = '#3572A5';
|
|
419
|
+
else if (isPhp)
|
|
420
|
+
moduleColor = '#4F5D95';
|
|
421
|
+
else if (isBlade)
|
|
422
|
+
moduleColor = '#FF2D20';
|
|
423
|
+
this.addNode({
|
|
424
|
+
id: moduleId,
|
|
425
|
+
label: path.basename(filePath),
|
|
426
|
+
type: 'module',
|
|
427
|
+
color: moduleColor,
|
|
428
|
+
filePath: filePath,
|
|
429
|
+
line: 1
|
|
430
|
+
});
|
|
431
|
+
if (isPython) {
|
|
432
|
+
this.analyzePythonFile(code, moduleId, filePath);
|
|
433
|
+
}
|
|
434
|
+
else if (isPhp) {
|
|
435
|
+
this.analyzePhpFile(code, moduleId, filePath);
|
|
436
|
+
}
|
|
437
|
+
else if (isBlade) {
|
|
438
|
+
this.analyzeBladeFile(code, moduleId, filePath);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
const ast = parse(code, {
|
|
442
|
+
loc: true,
|
|
443
|
+
range: true,
|
|
444
|
+
jsx: true
|
|
445
|
+
});
|
|
446
|
+
// Keep track of imports in this file to resolve calls
|
|
447
|
+
const fileImports = new Map(); // alias/name -> moduleId
|
|
448
|
+
this.traverseAST(ast, moduleId, filePath, moduleId, fileImports);
|
|
449
|
+
}
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
console.warn(`Failed to parse file: ${filePath}`, e);
|
|
454
|
+
// Clean up the node if it was added
|
|
455
|
+
if (moduleId && this.nodes.has(moduleId)) {
|
|
456
|
+
this.nodes.delete(moduleId);
|
|
457
|
+
}
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Parses Python files using the PythonParser.
|
|
463
|
+
*/
|
|
464
|
+
analyzePythonFile(code, moduleId, filePath) {
|
|
465
|
+
const pythonParser = new PythonParser();
|
|
466
|
+
const result = pythonParser.parseFile(filePath, code);
|
|
467
|
+
for (const cls of result.classes) {
|
|
468
|
+
const classId = `class:${moduleId}:${cls.name}`;
|
|
469
|
+
this.addNode({
|
|
470
|
+
id: classId,
|
|
471
|
+
label: cls.name,
|
|
472
|
+
type: 'class',
|
|
473
|
+
color: '#f8961e',
|
|
474
|
+
filePath: filePath,
|
|
475
|
+
line: cls.line
|
|
476
|
+
});
|
|
477
|
+
this.addLink({
|
|
478
|
+
source: moduleId,
|
|
479
|
+
target: classId,
|
|
480
|
+
type: 'contains'
|
|
481
|
+
});
|
|
482
|
+
for (const parent of cls.parents) {
|
|
483
|
+
const parentId = `class:${moduleId}:${parent}`;
|
|
484
|
+
this.addLink({
|
|
485
|
+
source: classId,
|
|
486
|
+
target: parentId,
|
|
487
|
+
type: 'import'
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const reversedClasses = [...result.classes].reverse();
|
|
492
|
+
for (const func of result.functions) {
|
|
493
|
+
const funcId = `function:${moduleId}:${func.name}`;
|
|
494
|
+
this.addNode({
|
|
495
|
+
id: funcId,
|
|
496
|
+
label: func.name,
|
|
497
|
+
type: 'function',
|
|
498
|
+
color: '#f72585',
|
|
499
|
+
filePath: filePath,
|
|
500
|
+
line: func.line
|
|
501
|
+
});
|
|
502
|
+
let linkSourceId = moduleId;
|
|
503
|
+
if (func.indent && func.indent > 0) {
|
|
504
|
+
// Find the most recent class defined before this function
|
|
505
|
+
const parentClass = reversedClasses
|
|
506
|
+
.find(cls => cls.line < func.line);
|
|
507
|
+
if (parentClass) {
|
|
508
|
+
linkSourceId = `class:${moduleId}:${parentClass.name}`;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
this.addLink({
|
|
512
|
+
source: linkSourceId,
|
|
513
|
+
target: funcId,
|
|
514
|
+
type: 'contains'
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
for (const variable of result.variables) {
|
|
518
|
+
const varId = `variable:${moduleId}:${variable.name}`;
|
|
519
|
+
this.addNode({
|
|
520
|
+
id: varId,
|
|
521
|
+
label: variable.name,
|
|
522
|
+
type: 'variable',
|
|
523
|
+
color: '#00ff88',
|
|
524
|
+
filePath: filePath,
|
|
525
|
+
line: variable.line
|
|
526
|
+
});
|
|
527
|
+
this.addLink({
|
|
528
|
+
source: moduleId,
|
|
529
|
+
target: varId,
|
|
530
|
+
type: 'contains'
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
for (const imp of result.imports) {
|
|
534
|
+
const importPathName = imp.source || imp.names[0];
|
|
535
|
+
if (!importPathName)
|
|
536
|
+
continue;
|
|
537
|
+
const resolvedModuleId = this.resolvePythonImportPath(importPathName, filePath);
|
|
538
|
+
const targetId = resolvedModuleId || `external:${importPathName}`;
|
|
539
|
+
this.addLink({
|
|
540
|
+
source: moduleId,
|
|
541
|
+
target: targetId,
|
|
542
|
+
type: 'import'
|
|
543
|
+
});
|
|
544
|
+
if (!resolvedModuleId) {
|
|
545
|
+
if (!this.nodes.has(targetId)) {
|
|
546
|
+
this.addNode({
|
|
547
|
+
id: targetId,
|
|
548
|
+
label: importPathName,
|
|
549
|
+
type: 'module',
|
|
550
|
+
color: '#7209b7'
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Build a simple scope tracker from functions list
|
|
556
|
+
const sortedFunctions = [...result.functions].sort((a, b) => a.line - b.line);
|
|
557
|
+
for (const call of result.calls) {
|
|
558
|
+
// Find which function this call is inside using binary search
|
|
559
|
+
let enclosingScope = moduleId;
|
|
560
|
+
let left = 0;
|
|
561
|
+
let right = sortedFunctions.length - 1;
|
|
562
|
+
let match = -1;
|
|
563
|
+
const callLine = call.line;
|
|
564
|
+
while (left <= right) {
|
|
565
|
+
const mid = (left + right) >> 1;
|
|
566
|
+
if (sortedFunctions[mid].line <= callLine) {
|
|
567
|
+
match = mid;
|
|
568
|
+
left = mid + 1;
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
right = mid - 1;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (match !== -1) {
|
|
575
|
+
enclosingScope = `function:${moduleId}:${sortedFunctions[match].name}`;
|
|
576
|
+
}
|
|
577
|
+
const targetId = `function:${moduleId}:${call.name}`;
|
|
578
|
+
// Don't add self-referencing calls
|
|
579
|
+
if (enclosingScope !== targetId) {
|
|
580
|
+
this.addLink({
|
|
581
|
+
source: enclosingScope,
|
|
582
|
+
target: targetId,
|
|
583
|
+
type: 'call'
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Parses PHP files using the PhpParser.
|
|
590
|
+
*/
|
|
591
|
+
analyzePhpFile(code, moduleId, filePath) {
|
|
592
|
+
const phpParser = new PhpParser();
|
|
593
|
+
const result = phpParser.parseFile(filePath, code);
|
|
594
|
+
// Classes, Interfaces, Traits, Enums
|
|
595
|
+
for (const cls of result.classes) {
|
|
596
|
+
const nodeType = cls.type === 'interface' ? 'class' : cls.type === 'trait' ? 'class' : 'class';
|
|
597
|
+
const classId = `class:${moduleId}:${cls.name}`;
|
|
598
|
+
const colorMap = {
|
|
599
|
+
class: '#f8961e',
|
|
600
|
+
interface: '#7209b7',
|
|
601
|
+
trait: '#06d6a0',
|
|
602
|
+
enum: '#ffd166'
|
|
603
|
+
};
|
|
604
|
+
this.addNode({
|
|
605
|
+
id: classId,
|
|
606
|
+
label: `${cls.name}`,
|
|
607
|
+
type: nodeType,
|
|
608
|
+
color: colorMap[cls.type] || '#f8961e',
|
|
609
|
+
filePath: filePath,
|
|
610
|
+
line: cls.line
|
|
611
|
+
});
|
|
612
|
+
this.addLink({ source: moduleId, target: classId, type: 'contains' });
|
|
613
|
+
// Extends
|
|
614
|
+
for (const parent of cls.parents) {
|
|
615
|
+
const parentId = `class:external:${parent}`;
|
|
616
|
+
if (!this.nodes.has(parentId)) {
|
|
617
|
+
this.addNode({ id: parentId, label: parent, type: 'class', color: '#f8961e' });
|
|
618
|
+
}
|
|
619
|
+
this.addLink({ source: classId, target: parentId, type: 'import' });
|
|
620
|
+
}
|
|
621
|
+
// Implements
|
|
622
|
+
for (const iface of cls.implements) {
|
|
623
|
+
const ifaceId = `class:external:${iface}`;
|
|
624
|
+
if (!this.nodes.has(ifaceId)) {
|
|
625
|
+
this.addNode({ id: ifaceId, label: iface, type: 'class', color: '#7209b7' });
|
|
626
|
+
}
|
|
627
|
+
this.addLink({ source: classId, target: ifaceId, type: 'import' });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Functions
|
|
631
|
+
for (const func of result.functions) {
|
|
632
|
+
const funcId = `function:${moduleId}:${func.name}`;
|
|
633
|
+
this.addNode({
|
|
634
|
+
id: funcId,
|
|
635
|
+
label: func.name,
|
|
636
|
+
type: 'function',
|
|
637
|
+
color: '#f72585',
|
|
638
|
+
filePath: filePath,
|
|
639
|
+
line: func.line
|
|
640
|
+
});
|
|
641
|
+
this.addLink({ source: moduleId, target: funcId, type: 'contains' });
|
|
642
|
+
}
|
|
643
|
+
// Variables (properties, constants)
|
|
644
|
+
for (const variable of result.variables) {
|
|
645
|
+
const varId = `variable:${moduleId}:${variable.name}`;
|
|
646
|
+
this.addNode({
|
|
647
|
+
id: varId,
|
|
648
|
+
label: variable.name,
|
|
649
|
+
type: 'variable',
|
|
650
|
+
color: '#00ff88',
|
|
651
|
+
filePath: filePath,
|
|
652
|
+
line: variable.line
|
|
653
|
+
});
|
|
654
|
+
this.addLink({ source: moduleId, target: varId, type: 'contains' });
|
|
655
|
+
}
|
|
656
|
+
// Use statements (imports)
|
|
657
|
+
for (const imp of result.imports) {
|
|
658
|
+
const importSourceId = `external:${imp.alias}`;
|
|
659
|
+
this.addLink({ source: moduleId, target: importSourceId, type: 'import' });
|
|
660
|
+
if (!this.nodes.has(importSourceId)) {
|
|
661
|
+
this.addNode({
|
|
662
|
+
id: importSourceId,
|
|
663
|
+
label: imp.alias,
|
|
664
|
+
type: 'module',
|
|
665
|
+
color: '#7209b7'
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// Method calls — only link to functions already discovered in the graph
|
|
670
|
+
const functionNodesByName = new Map();
|
|
671
|
+
for (const key of this.nodes.keys()) {
|
|
672
|
+
if (key.startsWith('function:')) {
|
|
673
|
+
const lastColonIdx = key.lastIndexOf(':');
|
|
674
|
+
if (lastColonIdx !== -1) {
|
|
675
|
+
const funcName = key.substring(lastColonIdx + 1);
|
|
676
|
+
let arr = functionNodesByName.get(funcName);
|
|
677
|
+
if (!arr) {
|
|
678
|
+
arr = [];
|
|
679
|
+
functionNodesByName.set(funcName, arr);
|
|
680
|
+
}
|
|
681
|
+
arr.push(key);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
for (const call of result.calls) {
|
|
686
|
+
// Search all known function nodes to find a match
|
|
687
|
+
const possibleTargets = functionNodesByName.get(call.name);
|
|
688
|
+
if (possibleTargets && possibleTargets.length > 0) {
|
|
689
|
+
this.addLink({ source: moduleId, target: possibleTargets[0], type: 'call' });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Parses Blade template files to extract template relationships.
|
|
695
|
+
*/
|
|
696
|
+
analyzeBladeFile(code, moduleId, filePath) {
|
|
697
|
+
const phpParser = new PhpParser();
|
|
698
|
+
const result = phpParser.parseBladeFile(filePath, code);
|
|
699
|
+
// @extends creates a dependency link
|
|
700
|
+
for (const ext of result.extends) {
|
|
701
|
+
const targetId = `blade:${ext.replace(/\./g, '/')}`;
|
|
702
|
+
if (!this.nodes.has(targetId)) {
|
|
703
|
+
this.addNode({ id: targetId, label: ext, type: 'module', color: '#FF2D20' });
|
|
704
|
+
}
|
|
705
|
+
this.addLink({ source: moduleId, target: targetId, type: 'import' });
|
|
706
|
+
}
|
|
707
|
+
// @include creates a dependency link
|
|
708
|
+
for (const inc of result.includes) {
|
|
709
|
+
const targetId = `blade:${inc.replace(/\./g, '/')}`;
|
|
710
|
+
if (!this.nodes.has(targetId)) {
|
|
711
|
+
this.addNode({ id: targetId, label: inc, type: 'module', color: '#FF2D20' });
|
|
712
|
+
}
|
|
713
|
+
this.addLink({ source: moduleId, target: targetId, type: 'import' });
|
|
714
|
+
}
|
|
715
|
+
// @component / <x-component>
|
|
716
|
+
for (const comp of result.components) {
|
|
717
|
+
const targetId = `component:${comp}`;
|
|
718
|
+
if (!this.nodes.has(targetId)) {
|
|
719
|
+
this.addNode({ id: targetId, label: comp, type: 'class', color: '#f8961e' });
|
|
720
|
+
}
|
|
721
|
+
this.addLink({ source: moduleId, target: targetId, type: 'import' });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
handleImportDeclaration(node, currentModuleId, filePath, fileImports) {
|
|
725
|
+
const importPath = node.source?.value;
|
|
726
|
+
if (typeof importPath === 'string') {
|
|
727
|
+
const targetModuleId = this.resolveImportPath(importPath, filePath);
|
|
728
|
+
this.addLink({
|
|
729
|
+
source: currentModuleId,
|
|
730
|
+
target: targetModuleId,
|
|
731
|
+
type: 'import'
|
|
732
|
+
});
|
|
733
|
+
if (node.specifiers) {
|
|
734
|
+
for (const specifier of node.specifiers) {
|
|
735
|
+
if (specifier.local?.name) {
|
|
736
|
+
fileImports.set(specifier.local.name, targetModuleId);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
// Ensure target module node exists (even if it's an external module for now)
|
|
741
|
+
if (!this.nodes.has(targetModuleId)) {
|
|
742
|
+
this.addNode({
|
|
743
|
+
id: targetModuleId,
|
|
744
|
+
label: importPath.split('/').pop() || importPath,
|
|
745
|
+
type: 'module',
|
|
746
|
+
color: '#7209b7' // external module color
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
handleFunctionDeclaration(node, currentModuleId, filePath, currentScopeId) {
|
|
752
|
+
if (node.id?.name) {
|
|
753
|
+
const funcId = `function:${currentModuleId}:${node.id.name}`;
|
|
754
|
+
this.addNode({
|
|
755
|
+
id: funcId,
|
|
756
|
+
label: node.id.name,
|
|
757
|
+
type: 'function',
|
|
758
|
+
color: '#f72585',
|
|
759
|
+
filePath: filePath,
|
|
760
|
+
line: node.loc?.start?.line
|
|
761
|
+
});
|
|
762
|
+
this.addLink({
|
|
763
|
+
source: currentScopeId,
|
|
764
|
+
target: funcId,
|
|
765
|
+
type: 'contains'
|
|
766
|
+
});
|
|
767
|
+
return funcId;
|
|
768
|
+
}
|
|
769
|
+
return currentScopeId;
|
|
770
|
+
}
|
|
771
|
+
handleClassDeclaration(node, currentModuleId, filePath, currentScopeId) {
|
|
772
|
+
if (node.id?.name) {
|
|
773
|
+
const classId = `class:${currentModuleId}:${node.id.name}`;
|
|
774
|
+
this.addNode({
|
|
775
|
+
id: classId,
|
|
776
|
+
label: node.id.name,
|
|
777
|
+
type: 'class',
|
|
778
|
+
color: '#f8961e',
|
|
779
|
+
filePath: filePath,
|
|
780
|
+
line: node.loc?.start?.line
|
|
781
|
+
});
|
|
782
|
+
this.addLink({
|
|
783
|
+
source: currentScopeId,
|
|
784
|
+
target: classId,
|
|
785
|
+
type: 'contains'
|
|
786
|
+
});
|
|
787
|
+
return classId;
|
|
788
|
+
}
|
|
789
|
+
return currentScopeId;
|
|
790
|
+
}
|
|
791
|
+
handleMethodDefinition(node, currentModuleId, filePath, currentScopeId) {
|
|
792
|
+
if (node.key?.name) {
|
|
793
|
+
const methodId = `function:${currentModuleId}:${node.key.name}`;
|
|
794
|
+
this.addNode({
|
|
795
|
+
id: methodId,
|
|
796
|
+
label: node.key.name,
|
|
797
|
+
type: 'function',
|
|
798
|
+
color: '#f72585',
|
|
799
|
+
filePath: filePath,
|
|
800
|
+
line: node.loc?.start?.line
|
|
801
|
+
});
|
|
802
|
+
this.addLink({
|
|
803
|
+
source: currentScopeId,
|
|
804
|
+
target: methodId,
|
|
805
|
+
type: 'contains'
|
|
806
|
+
});
|
|
807
|
+
return methodId;
|
|
808
|
+
}
|
|
809
|
+
return currentScopeId;
|
|
810
|
+
}
|
|
811
|
+
handleVariableDeclarator(node, currentModuleId, filePath, currentScopeId) {
|
|
812
|
+
if (node.id?.name) {
|
|
813
|
+
const varName = node.id.name;
|
|
814
|
+
if (node.init && (node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression')) {
|
|
815
|
+
const funcId = `function:${currentModuleId}:${varName}`;
|
|
816
|
+
this.addNode({
|
|
817
|
+
id: funcId,
|
|
818
|
+
label: varName,
|
|
819
|
+
type: 'function',
|
|
820
|
+
color: '#f72585',
|
|
821
|
+
filePath: filePath,
|
|
822
|
+
line: node.loc?.start?.line
|
|
823
|
+
});
|
|
824
|
+
this.addLink({
|
|
825
|
+
source: currentScopeId,
|
|
826
|
+
target: funcId,
|
|
827
|
+
type: 'contains'
|
|
828
|
+
});
|
|
829
|
+
return funcId;
|
|
830
|
+
}
|
|
831
|
+
else if (currentScopeId === currentModuleId) {
|
|
832
|
+
// Top-level variable
|
|
833
|
+
const varId = `variable:${currentModuleId}:${varName}`;
|
|
834
|
+
this.addNode({
|
|
835
|
+
id: varId,
|
|
836
|
+
label: varName,
|
|
837
|
+
type: 'variable',
|
|
838
|
+
color: '#00ff88', // Green for variables
|
|
839
|
+
filePath: filePath,
|
|
840
|
+
line: node.loc?.start?.line
|
|
841
|
+
});
|
|
842
|
+
this.addLink({
|
|
843
|
+
source: currentScopeId,
|
|
844
|
+
target: varId,
|
|
845
|
+
type: 'contains'
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return currentScopeId;
|
|
850
|
+
}
|
|
851
|
+
handleCallExpression(node, currentModuleId, currentScopeId, fileImports) {
|
|
852
|
+
let calleeName = '';
|
|
853
|
+
let objectName = '';
|
|
854
|
+
if (node.callee.type === 'Identifier') {
|
|
855
|
+
calleeName = node.callee.name;
|
|
856
|
+
}
|
|
857
|
+
else if (node.callee.type === 'MemberExpression' && node.callee.property?.name) {
|
|
858
|
+
calleeName = node.callee.property.name;
|
|
859
|
+
if (node.callee.object?.name) {
|
|
860
|
+
objectName = node.callee.object.name;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (calleeName) {
|
|
864
|
+
let targetId = '';
|
|
865
|
+
if (objectName && fileImports.has(objectName)) {
|
|
866
|
+
// It's a method call on an imported namespace/object
|
|
867
|
+
const targetModule = fileImports.get(objectName);
|
|
868
|
+
targetId = `function:${targetModule}:${calleeName}`;
|
|
869
|
+
}
|
|
870
|
+
else if (fileImports.has(calleeName)) {
|
|
871
|
+
// It's a direct call to an imported function
|
|
872
|
+
const targetModule = fileImports.get(calleeName);
|
|
873
|
+
targetId = `function:${targetModule}:${calleeName}`;
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
// It's a local call
|
|
877
|
+
targetId = `function:${currentModuleId}:${calleeName}`;
|
|
878
|
+
}
|
|
879
|
+
this.addLink({
|
|
880
|
+
source: currentScopeId,
|
|
881
|
+
target: targetId,
|
|
882
|
+
type: 'call'
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Traverses the AST recursively to extract modules, functions, classes, and variables.
|
|
888
|
+
* Also detects function calls and updates links.
|
|
889
|
+
*/
|
|
890
|
+
traverseAST(node, currentModuleId, filePath, currentScopeId, fileImports) {
|
|
891
|
+
if (!node)
|
|
892
|
+
return;
|
|
893
|
+
if (Array.isArray(node)) {
|
|
894
|
+
node.forEach(child => this.traverseAST(child, currentModuleId, filePath, currentScopeId, fileImports));
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
let nextScopeId = currentScopeId;
|
|
898
|
+
if (node.type === 'ImportDeclaration') {
|
|
899
|
+
this.handleImportDeclaration(node, currentModuleId, filePath, fileImports);
|
|
900
|
+
}
|
|
901
|
+
else if (node.type === 'FunctionDeclaration') {
|
|
902
|
+
nextScopeId = this.handleFunctionDeclaration(node, currentModuleId, filePath, currentScopeId);
|
|
903
|
+
}
|
|
904
|
+
else if (node.type === 'ClassDeclaration') {
|
|
905
|
+
nextScopeId = this.handleClassDeclaration(node, currentModuleId, filePath, currentScopeId);
|
|
906
|
+
}
|
|
907
|
+
else if (node.type === 'MethodDefinition') {
|
|
908
|
+
nextScopeId = this.handleMethodDefinition(node, currentModuleId, filePath, currentScopeId);
|
|
909
|
+
}
|
|
910
|
+
else if (node.type === 'VariableDeclarator') {
|
|
911
|
+
nextScopeId = this.handleVariableDeclarator(node, currentModuleId, filePath, currentScopeId);
|
|
912
|
+
}
|
|
913
|
+
else if (node.type === 'CallExpression') {
|
|
914
|
+
this.handleCallExpression(node, currentModuleId, currentScopeId, fileImports);
|
|
915
|
+
}
|
|
916
|
+
Object.keys(node).forEach(key => {
|
|
917
|
+
if (key !== 'loc' && key !== 'range' && typeof node[key] === 'object') {
|
|
918
|
+
this.traverseAST(node[key], currentModuleId, filePath, nextScopeId, fileImports);
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Resolves the actual module path checking for index files and standard file extensions.
|
|
924
|
+
*/
|
|
925
|
+
resolveImportPath(importPath, currentFilePath) {
|
|
926
|
+
if (importPath.startsWith('.')) {
|
|
927
|
+
try {
|
|
928
|
+
let absolutePath = path.resolve(path.dirname(currentFilePath), importPath);
|
|
929
|
+
let foundPath = absolutePath;
|
|
930
|
+
const extensions = this.fileExtensions;
|
|
931
|
+
let matched = false;
|
|
932
|
+
if (fs.existsSync(absolutePath)) {
|
|
933
|
+
const stat = fs.statSync(absolutePath);
|
|
934
|
+
if (stat.isDirectory()) {
|
|
935
|
+
for (const ext of extensions) {
|
|
936
|
+
const indexPath = path.join(absolutePath, `index${ext}`);
|
|
937
|
+
if (fs.existsSync(indexPath)) {
|
|
938
|
+
foundPath = indexPath;
|
|
939
|
+
matched = true;
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
matched = true;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (!matched) {
|
|
949
|
+
for (const ext of extensions) {
|
|
950
|
+
const extPath = `${absolutePath}${ext}`;
|
|
951
|
+
if (fs.existsSync(extPath)) {
|
|
952
|
+
foundPath = extPath;
|
|
953
|
+
matched = true;
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
const relativeToWorkspace = path.relative(this.workspaceRoot, foundPath);
|
|
959
|
+
// Normalize slashes
|
|
960
|
+
return `module:${relativeToWorkspace.replace(/\\/g, '/')}`;
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
return `external:${importPath}`;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return `external:${importPath}`;
|
|
967
|
+
}
|
|
968
|
+
resolvePythonImportPath(importPath, currentFilePath) {
|
|
969
|
+
let leadingDots = '';
|
|
970
|
+
let rest = importPath;
|
|
971
|
+
while (rest.startsWith('.')) {
|
|
972
|
+
leadingDots += '.';
|
|
973
|
+
rest = rest.substring(1);
|
|
974
|
+
}
|
|
975
|
+
let searchDirs = [];
|
|
976
|
+
if (leadingDots.length > 0) {
|
|
977
|
+
let targetDir = path.dirname(currentFilePath);
|
|
978
|
+
for (let i = 1; i < leadingDots.length; i++) {
|
|
979
|
+
targetDir = path.dirname(targetDir);
|
|
980
|
+
}
|
|
981
|
+
searchDirs = [targetDir];
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
searchDirs = [this.workspaceRoot, path.dirname(currentFilePath)];
|
|
985
|
+
}
|
|
986
|
+
const subPath = rest.replace(/\./g, '/');
|
|
987
|
+
for (const baseDir of searchDirs) {
|
|
988
|
+
const targetPath = path.join(baseDir, subPath);
|
|
989
|
+
const pyPath = `${targetPath}.py`;
|
|
990
|
+
if (fs.existsSync(pyPath)) {
|
|
991
|
+
return `module:${path.relative(this.workspaceRoot, pyPath).replace(/\\/g, '/')}`;
|
|
992
|
+
}
|
|
993
|
+
const initPyPath = path.join(targetPath, '__init__.py');
|
|
994
|
+
if (fs.existsSync(initPyPath)) {
|
|
995
|
+
return `module:${path.relative(this.workspaceRoot, initPyPath).replace(/\\/g, '/')}`;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
addNode(node) {
|
|
1001
|
+
if (node.id.startsWith('external:') || node.id.includes(':external:')) {
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
if (node.filePath && path.isAbsolute(node.filePath)) {
|
|
1005
|
+
node.filePath = path.relative(this.workspaceRoot, node.filePath).replace(/\\/g, '/');
|
|
1006
|
+
}
|
|
1007
|
+
if (!this.nodes.has(node.id)) {
|
|
1008
|
+
this.nodes.set(node.id, node);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
addLink(link) {
|
|
1012
|
+
if (link.source.startsWith('external:') ||
|
|
1013
|
+
link.target.startsWith('external:') ||
|
|
1014
|
+
link.source.includes(':external:') ||
|
|
1015
|
+
link.target.includes(':external:')) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
this.links.push(link);
|
|
1019
|
+
}
|
|
1020
|
+
generateAIInsights(graph) {
|
|
1021
|
+
const insights = [];
|
|
1022
|
+
// Mock AI Insights generation based on simple heuristics
|
|
1023
|
+
// 1. Large files / God objects
|
|
1024
|
+
const modulesWithManyFunctions = Array.from(this.nodes.values()).filter(n => {
|
|
1025
|
+
if (n.type !== 'module')
|
|
1026
|
+
return false;
|
|
1027
|
+
const functionCount = graph.links.filter(l => l.source === n.id && l.type === 'import' && l.target.startsWith('function')).length;
|
|
1028
|
+
return functionCount > 10; // threshold
|
|
1029
|
+
});
|
|
1030
|
+
if (modulesWithManyFunctions.length > 0) {
|
|
1031
|
+
insights.push({
|
|
1032
|
+
id: 'i-1',
|
|
1033
|
+
type: 'refactor',
|
|
1034
|
+
title: 'God Object Detected',
|
|
1035
|
+
description: `Found ${modulesWithManyFunctions.length} modules containing a large number of functions. Consider splitting them.`,
|
|
1036
|
+
severity: 'high',
|
|
1037
|
+
affectedNodes: modulesWithManyFunctions.map(n => n.id)
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
// 2. High coupling
|
|
1041
|
+
const moduleDependencies = new Map();
|
|
1042
|
+
graph.links.forEach(l => {
|
|
1043
|
+
if (l.type === 'import' && l.source.startsWith('module:') && l.target.startsWith('module:')) {
|
|
1044
|
+
moduleDependencies.set(l.source, (moduleDependencies.get(l.source) || 0) + 1);
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
const highlyCoupled = Array.from(moduleDependencies.entries()).filter(([_, count]) => count > 15).map(([id]) => id);
|
|
1048
|
+
if (highlyCoupled.length > 0) {
|
|
1049
|
+
insights.push({
|
|
1050
|
+
id: 'i-2',
|
|
1051
|
+
type: 'architecture',
|
|
1052
|
+
title: 'High Coupling',
|
|
1053
|
+
description: `Some modules have excessive external dependencies, making them hard to test.`,
|
|
1054
|
+
severity: 'medium',
|
|
1055
|
+
affectedNodes: highlyCoupled
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
// Generic fallback insight if none found
|
|
1059
|
+
if (insights.length === 0) {
|
|
1060
|
+
insights.push({
|
|
1061
|
+
id: 'i-default',
|
|
1062
|
+
type: 'maintainability',
|
|
1063
|
+
title: 'Clean Architecture',
|
|
1064
|
+
description: 'No major structural issues detected in the analyzed scope.',
|
|
1065
|
+
severity: 'low',
|
|
1066
|
+
affectedNodes: []
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
return insights;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
//# sourceMappingURL=parser.js.map
|