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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +338 -0
  3. package/dist/index.js +261 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/src/analyzer/parser.js +1072 -0
  6. package/dist/src/analyzer/parser.js.map +1 -0
  7. package/dist/src/analyzer/parser.test.js +73 -0
  8. package/dist/src/analyzer/parser.test.js.map +1 -0
  9. package/dist/src/analyzer/phpParser.js +147 -0
  10. package/dist/src/analyzer/phpParser.js.map +1 -0
  11. package/dist/src/analyzer/pythonParser.js +185 -0
  12. package/dist/src/analyzer/pythonParser.js.map +1 -0
  13. package/dist/src/analyzer/types.js +2 -0
  14. package/dist/src/analyzer/types.js.map +1 -0
  15. package/dist/src/context.js +3 -0
  16. package/dist/src/context.js.map +1 -0
  17. package/dist/src/memoryGenerator.js +293 -0
  18. package/dist/src/memoryGenerator.js.map +1 -0
  19. package/dist/src/oracleDatabase.js +298 -0
  20. package/dist/src/oracleDatabase.js.map +1 -0
  21. package/dist/src/presentation/httpServer.js +306 -0
  22. package/dist/src/presentation/httpServer.js.map +1 -0
  23. package/dist/src/presentation/mcpServer.js +1487 -0
  24. package/dist/src/presentation/mcpServer.js.map +1 -0
  25. package/dist/src/repositories.js +144 -0
  26. package/dist/src/repositories.js.map +1 -0
  27. package/dist/src/securityScanner.js +69 -0
  28. package/dist/src/securityScanner.js.map +1 -0
  29. package/dist/src/services/authService.js +24 -0
  30. package/dist/src/services/authService.js.map +1 -0
  31. package/dist/src/services/dreamingService.js +119 -0
  32. package/dist/src/services/dreamingService.js.map +1 -0
  33. package/dist/src/services/dreamingService.test.js +179 -0
  34. package/dist/src/services/dreamingService.test.js.map +1 -0
  35. package/dist/src/services/projectService.js +1068 -0
  36. package/dist/src/services/projectService.js.map +1 -0
  37. package/dist/src/services/projectService.test.js +217 -0
  38. package/dist/src/services/projectService.test.js.map +1 -0
  39. package/dist/src/services/watcherService.js +164 -0
  40. package/dist/src/services/watcherService.js.map +1 -0
  41. package/dist/src/services/watcherService.test.js +65 -0
  42. package/dist/src/services/watcherService.test.js.map +1 -0
  43. package/dist/src/types.js +2 -0
  44. package/dist/src/types.js.map +1 -0
  45. 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