arcvision 0.2.2 → 0.2.3
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/arcvision.context.json +273 -7610
- package/dist/index.js +873 -3
- package/package.json +1 -1
- package/src/core/di-detector.js +202 -0
- package/src/core/method-tracker.js +174 -0
- package/src/core/parser-enhanced.js +73 -0
- package/src/core/react-nextjs-detector.js +245 -0
- package/src/core/scanner.js +84 -58
- package/src/core/semantic-analyzer.js +204 -0
- package/src/core/type-analyzer.js +272 -0
- package/src/engine/context_builder.js +34 -11
package/src/core/scanner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { glob } = require('glob');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs');
|
|
4
|
-
const parser = require('./parser');
|
|
4
|
+
const parser = require('./parser-enhanced'); // Use enhanced parser
|
|
5
5
|
const pluginManager = require('../plugins/plugin-manager');
|
|
6
6
|
const { loadTSConfig } = require('./tsconfig-utils');
|
|
7
7
|
const { resolveImport } = require('./path-resolver');
|
|
@@ -12,10 +12,13 @@ const { buildContext } = require('../engine/context_builder');
|
|
|
12
12
|
const { validateContext } = require('../engine/context_validator');
|
|
13
13
|
const { sortContext } = require('../engine/context_sorter');
|
|
14
14
|
|
|
15
|
+
// Import semantic analyzer for cross-file analysis
|
|
16
|
+
const { analyzeSemantics } = require('./semantic-analyzer');
|
|
17
|
+
|
|
15
18
|
async function scan(directory) {
|
|
16
19
|
// Normalize helper
|
|
17
20
|
const normalize = p => p.replace(/\\/g, '/');
|
|
18
|
-
|
|
21
|
+
|
|
19
22
|
const options = {
|
|
20
23
|
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**'],
|
|
21
24
|
cwd: directory,
|
|
@@ -42,10 +45,10 @@ async function scan(directory) {
|
|
|
42
45
|
try {
|
|
43
46
|
const relativePath = path.relative(directory, file);
|
|
44
47
|
const normalizedRelativePath = normalize(relativePath);
|
|
45
|
-
|
|
48
|
+
|
|
46
49
|
// Handle different file types appropriately
|
|
47
50
|
let metadata;
|
|
48
|
-
|
|
51
|
+
|
|
49
52
|
if (file.endsWith('.json')) {
|
|
50
53
|
// Handle JSON files separately
|
|
51
54
|
try {
|
|
@@ -71,7 +74,7 @@ async function scan(directory) {
|
|
|
71
74
|
|
|
72
75
|
// Process with plugins
|
|
73
76
|
metadata = await pluginManager.processFile(file, metadata);
|
|
74
|
-
|
|
77
|
+
|
|
75
78
|
// Count imports found in this file
|
|
76
79
|
if (metadata.imports && Array.isArray(metadata.imports)) {
|
|
77
80
|
totalImportsFound += metadata.imports.length;
|
|
@@ -90,21 +93,26 @@ async function scan(directory) {
|
|
|
90
93
|
console.warn(`⚠️ Failed to process ${file} — file skipped, you should check manually (${e.message})`);
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
|
-
|
|
96
|
+
|
|
97
|
+
// NEW: Perform semantic analysis to build cross-file usage edges
|
|
98
|
+
console.log('Performing semantic analysis...');
|
|
99
|
+
const semanticResults = analyzeSemantics(architectureMap.nodes);
|
|
100
|
+
console.log(`Semantic analysis complete: ${semanticResults.stats.totalSymbols} symbols, ${semanticResults.stats.totalUsages} usage edges`);
|
|
101
|
+
|
|
94
102
|
// Load tsconfig for path resolution
|
|
95
103
|
const tsconfig = loadTSConfig(directory);
|
|
96
|
-
|
|
104
|
+
|
|
97
105
|
// Create a mapping of all possible normalized paths that could match imports
|
|
98
106
|
// This includes both the direct file paths and possible variations
|
|
99
107
|
const allPossiblePaths = new Set();
|
|
100
108
|
const pathToNodeIdMap = new Map(); // This maps normalized paths to node IDs
|
|
101
|
-
|
|
109
|
+
|
|
102
110
|
architectureMap.nodes.forEach(node => {
|
|
103
111
|
const normalizedPath = normalize(node.id);
|
|
104
112
|
allPossiblePaths.add(normalizedPath);
|
|
105
113
|
pathToNodeIdMap.set(normalizedPath, node.id);
|
|
106
114
|
});
|
|
107
|
-
|
|
115
|
+
|
|
108
116
|
// Process imports to create edges
|
|
109
117
|
let unresolvedImports = 0;
|
|
110
118
|
let resolvedImports = 0;
|
|
@@ -113,7 +121,7 @@ async function scan(directory) {
|
|
|
113
121
|
node.metadata.imports.forEach(imp => {
|
|
114
122
|
if (imp.source && typeof imp.source === 'string') {
|
|
115
123
|
let targetFound = false;
|
|
116
|
-
|
|
124
|
+
|
|
117
125
|
try {
|
|
118
126
|
// First, check if imp.source directly matches any node path (exact match)
|
|
119
127
|
if (allPossiblePaths.has(imp.source)) {
|
|
@@ -125,7 +133,7 @@ async function scan(directory) {
|
|
|
125
133
|
resolvedImports++;
|
|
126
134
|
targetFound = true;
|
|
127
135
|
}
|
|
128
|
-
|
|
136
|
+
|
|
129
137
|
if (!targetFound) {
|
|
130
138
|
// Try to resolve the import path using the resolver
|
|
131
139
|
const resolvedPath = resolveImport(
|
|
@@ -134,12 +142,12 @@ async function scan(directory) {
|
|
|
134
142
|
directory,
|
|
135
143
|
tsconfig
|
|
136
144
|
);
|
|
137
|
-
|
|
145
|
+
|
|
138
146
|
if (resolvedPath) {
|
|
139
147
|
// Convert resolved absolute path back to relative path
|
|
140
148
|
const relativeResolvedPath = path.relative(directory, resolvedPath);
|
|
141
149
|
const normalizedResolvedPath = normalize(relativeResolvedPath);
|
|
142
|
-
|
|
150
|
+
|
|
143
151
|
// Check if the resolved file exists in our scanned files
|
|
144
152
|
if (allPossiblePaths.has(normalizedResolvedPath)) {
|
|
145
153
|
architectureMap.edges.push({
|
|
@@ -152,7 +160,7 @@ async function scan(directory) {
|
|
|
152
160
|
}
|
|
153
161
|
}
|
|
154
162
|
}
|
|
155
|
-
|
|
163
|
+
|
|
156
164
|
if (!targetFound) {
|
|
157
165
|
// As a fallback, check if the import path could match by simple relative path calculation
|
|
158
166
|
// For example, if node is 'src/core/scanner.js' and import is './parser.js',
|
|
@@ -162,7 +170,7 @@ async function scan(directory) {
|
|
|
162
170
|
const calculatedAbsolutePath = path.resolve(baseDir, imp.source);
|
|
163
171
|
const calculatedRelativePath = path.relative(directory, calculatedAbsolutePath);
|
|
164
172
|
const calculatedNormalizedPath = normalize(calculatedRelativePath);
|
|
165
|
-
|
|
173
|
+
|
|
166
174
|
if (allPossiblePaths.has(calculatedNormalizedPath)) {
|
|
167
175
|
architectureMap.edges.push({
|
|
168
176
|
source: normalize(node.id),
|
|
@@ -177,27 +185,27 @@ async function scan(directory) {
|
|
|
177
185
|
// If path calculation fails, continue to unresolved
|
|
178
186
|
}
|
|
179
187
|
}
|
|
180
|
-
|
|
188
|
+
|
|
181
189
|
if (!targetFound) {
|
|
182
190
|
// Another fallback: if import starts with ./ or ../, try to find a match
|
|
183
191
|
// by appending common extensions to the import path
|
|
184
192
|
if (imp.source.startsWith('./') || imp.source.startsWith('../')) {
|
|
185
193
|
// Try various extensions that might match
|
|
186
194
|
const extensions = ['', '.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
|
|
187
|
-
|
|
195
|
+
|
|
188
196
|
for (const ext of extensions) {
|
|
189
197
|
let testPath = imp.source;
|
|
190
198
|
if (!imp.source.endsWith(ext)) {
|
|
191
199
|
testPath = imp.source + ext;
|
|
192
200
|
}
|
|
193
|
-
|
|
201
|
+
|
|
194
202
|
// Calculate the relative path from the project directory
|
|
195
203
|
try {
|
|
196
204
|
const baseDir = path.dirname(path.join(directory, node.id));
|
|
197
205
|
const calculatedAbsolutePath = path.resolve(baseDir, testPath);
|
|
198
206
|
const calculatedRelativePath = path.relative(directory, calculatedAbsolutePath);
|
|
199
207
|
const calculatedNormalizedPath = normalize(calculatedRelativePath);
|
|
200
|
-
|
|
208
|
+
|
|
201
209
|
if (allPossiblePaths.has(calculatedNormalizedPath)) {
|
|
202
210
|
architectureMap.edges.push({
|
|
203
211
|
source: normalize(node.id),
|
|
@@ -215,7 +223,7 @@ async function scan(directory) {
|
|
|
215
223
|
}
|
|
216
224
|
}
|
|
217
225
|
}
|
|
218
|
-
|
|
226
|
+
|
|
219
227
|
if (!targetFound) {
|
|
220
228
|
// Additional fallback: Check for index files when importing a directory
|
|
221
229
|
if (imp.source.startsWith('./') || imp.source.startsWith('../')) {
|
|
@@ -223,14 +231,14 @@ async function scan(directory) {
|
|
|
223
231
|
const baseDir = path.dirname(path.join(directory, node.id));
|
|
224
232
|
const calculatedAbsolutePath = path.resolve(baseDir, imp.source);
|
|
225
233
|
const directoryPath = calculatedAbsolutePath;
|
|
226
|
-
|
|
234
|
+
|
|
227
235
|
// Check for index files in the directory
|
|
228
236
|
const indexFiles = ['index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
|
|
229
237
|
for (const indexFile of indexFiles) {
|
|
230
238
|
const indexPath = path.join(directoryPath, indexFile);
|
|
231
239
|
const indexRelativePath = path.relative(directory, indexPath);
|
|
232
240
|
const indexNormalizedPath = normalize(indexRelativePath);
|
|
233
|
-
|
|
241
|
+
|
|
234
242
|
if (allPossiblePaths.has(indexNormalizedPath)) {
|
|
235
243
|
architectureMap.edges.push({
|
|
236
244
|
source: normalize(node.id),
|
|
@@ -248,7 +256,7 @@ async function scan(directory) {
|
|
|
248
256
|
}
|
|
249
257
|
}
|
|
250
258
|
}
|
|
251
|
-
|
|
259
|
+
|
|
252
260
|
if (!targetFound) {
|
|
253
261
|
// Additional fallback: Check for absolute imports that might map to common directories
|
|
254
262
|
// For example, if tsconfig has paths like "@/*": ["src/*"], an import like "@/utils/helper"
|
|
@@ -256,13 +264,13 @@ async function scan(directory) {
|
|
|
256
264
|
if (imp.source.startsWith('@') || imp.source.startsWith('/') || !imp.source.startsWith('.')) {
|
|
257
265
|
// Try to match against common source directories
|
|
258
266
|
const commonSourceDirs = ['src', 'app', 'lib', 'components', 'utils', 'services', 'assets'];
|
|
259
|
-
|
|
267
|
+
|
|
260
268
|
for (const srcDir of commonSourceDirs) {
|
|
261
269
|
// Try appending to common source directories
|
|
262
270
|
const potentialPath = path.join(srcDir, imp.source);
|
|
263
271
|
const potentialRelativePath = path.relative(directory, path.resolve(directory, potentialPath));
|
|
264
272
|
const potentialNormalizedPath = normalize(potentialRelativePath);
|
|
265
|
-
|
|
273
|
+
|
|
266
274
|
if (allPossiblePaths.has(potentialNormalizedPath)) {
|
|
267
275
|
architectureMap.edges.push({
|
|
268
276
|
source: normalize(node.id),
|
|
@@ -273,11 +281,11 @@ async function scan(directory) {
|
|
|
273
281
|
targetFound = true;
|
|
274
282
|
break; // Found a match, exit the loop
|
|
275
283
|
}
|
|
276
|
-
|
|
284
|
+
|
|
277
285
|
// Also try with extensions
|
|
278
286
|
const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs',
|
|
279
|
-
|
|
280
|
-
|
|
287
|
+
'index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
|
|
288
|
+
|
|
281
289
|
for (const ext of extensions) {
|
|
282
290
|
let testPath;
|
|
283
291
|
if (ext.startsWith('index')) {
|
|
@@ -285,10 +293,10 @@ async function scan(directory) {
|
|
|
285
293
|
} else {
|
|
286
294
|
testPath = path.join(srcDir, imp.source + ext);
|
|
287
295
|
}
|
|
288
|
-
|
|
296
|
+
|
|
289
297
|
const testRelativePath = path.relative(directory, path.resolve(directory, testPath));
|
|
290
298
|
const testNormalizedPath = normalize(testRelativePath);
|
|
291
|
-
|
|
299
|
+
|
|
292
300
|
if (allPossiblePaths.has(testNormalizedPath)) {
|
|
293
301
|
architectureMap.edges.push({
|
|
294
302
|
source: normalize(node.id),
|
|
@@ -300,12 +308,12 @@ async function scan(directory) {
|
|
|
300
308
|
break; // Found a match, exit the loops
|
|
301
309
|
}
|
|
302
310
|
}
|
|
303
|
-
|
|
311
|
+
|
|
304
312
|
if (targetFound) break;
|
|
305
313
|
}
|
|
306
314
|
}
|
|
307
315
|
}
|
|
308
|
-
|
|
316
|
+
|
|
309
317
|
if (!targetFound) {
|
|
310
318
|
// Additional fallback: Try to handle barrel files (index.js that exports from other files)
|
|
311
319
|
// If import is '@/components/Header' but only '@/components/index.js' exists
|
|
@@ -317,17 +325,17 @@ async function scan(directory) {
|
|
|
317
325
|
// Remove the last part and add index
|
|
318
326
|
const dirParts = parts.slice(0, -1);
|
|
319
327
|
const fileName = parts[parts.length - 1];
|
|
320
|
-
|
|
328
|
+
|
|
321
329
|
// Look for patterns like: components/index.js exporting { Header }
|
|
322
330
|
// or components/index.js containing export * from './Header'
|
|
323
331
|
const dirPath = dirParts.join('/') + '/index';
|
|
324
332
|
const indexFiles = ['index.js', 'index.ts', 'index.jsx', 'index.tsx', 'index.mjs', 'index.cjs'];
|
|
325
|
-
|
|
333
|
+
|
|
326
334
|
for (const indexFile of indexFiles) {
|
|
327
335
|
const indexPath = path.join(dirParts.join('/'), indexFile);
|
|
328
336
|
const indexRelativePath = path.relative(directory, path.resolve(directory, indexPath));
|
|
329
337
|
const indexNormalizedPath = normalize(indexRelativePath);
|
|
330
|
-
|
|
338
|
+
|
|
331
339
|
if (allPossiblePaths.has(indexNormalizedPath)) {
|
|
332
340
|
architectureMap.edges.push({
|
|
333
341
|
source: normalize(node.id),
|
|
@@ -346,7 +354,7 @@ async function scan(directory) {
|
|
|
346
354
|
}
|
|
347
355
|
}
|
|
348
356
|
}
|
|
349
|
-
|
|
357
|
+
|
|
350
358
|
if (!targetFound) {
|
|
351
359
|
// Additional fallback: Check for exact path matches with different capitalization
|
|
352
360
|
// This handles cases where import uses different casing than actual file
|
|
@@ -369,7 +377,7 @@ async function scan(directory) {
|
|
|
369
377
|
// Even if there's an error processing this import, increment unresolved counter
|
|
370
378
|
unresolvedImports++;
|
|
371
379
|
}
|
|
372
|
-
|
|
380
|
+
|
|
373
381
|
if (!targetFound) {
|
|
374
382
|
// Track imports that couldn't be matched to existing nodes
|
|
375
383
|
unresolvedImports++;
|
|
@@ -380,35 +388,53 @@ async function scan(directory) {
|
|
|
380
388
|
});
|
|
381
389
|
console.log('RESOLVED IMPORTS:', resolvedImports);
|
|
382
390
|
console.log('UNRESOLVED IMPORTS:', unresolvedImports);
|
|
383
|
-
console.log('EDGES CREATED:', architectureMap.edges.length);
|
|
391
|
+
console.log('IMPORT EDGES CREATED:', architectureMap.edges.length);
|
|
392
|
+
|
|
393
|
+
// NEW: Add semantic usage edges
|
|
394
|
+
if (semanticResults && semanticResults.usageEdges) {
|
|
395
|
+
semanticResults.usageEdges.forEach(usageEdge => {
|
|
396
|
+
// Only add if both source and target exist in our nodes
|
|
397
|
+
const sourceExists = architectureMap.nodes.some(n => n.id === usageEdge.source);
|
|
398
|
+
const targetExists = architectureMap.nodes.some(n => n.id === usageEdge.target);
|
|
399
|
+
|
|
400
|
+
if (sourceExists && targetExists) {
|
|
401
|
+
architectureMap.edges.push({
|
|
402
|
+
source: normalize(usageEdge.source),
|
|
403
|
+
target: normalize(usageEdge.target),
|
|
404
|
+
type: usageEdge.type // 'function_call', 'constructor_call', 'component_usage'
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
console.log('TOTAL EDGES (with usage):', architectureMap.edges.length);
|
|
384
410
|
|
|
385
411
|
// Calculate blast radius for each file
|
|
386
412
|
const reverseGraph = buildReverseDependencyGraph(architectureMap);
|
|
387
413
|
const blastRadiusMap = computeBlastRadius(reverseGraph);
|
|
388
|
-
|
|
414
|
+
|
|
389
415
|
// Add blast_radius to each node
|
|
390
416
|
architectureMap.nodes.forEach(node => {
|
|
391
417
|
node.metadata.blast_radius = blastRadiusMap[node.id] || 0;
|
|
392
418
|
});
|
|
393
|
-
|
|
419
|
+
|
|
394
420
|
console.log('BEFORE DEDUPLICATION EDGES:', architectureMap.edges.length);
|
|
395
|
-
|
|
421
|
+
|
|
396
422
|
// Process edges to ensure they match existing nodes and remove duplicates
|
|
397
423
|
const validEdges = [];
|
|
398
424
|
const edgeSet = new Set(); // To track unique edges
|
|
399
|
-
|
|
425
|
+
|
|
400
426
|
architectureMap.edges.forEach(edge => {
|
|
401
427
|
// Normalize both source and target paths for consistent matching
|
|
402
428
|
const normalizedSource = normalize(edge.source);
|
|
403
429
|
const normalizedTarget = normalize(edge.target);
|
|
404
|
-
|
|
430
|
+
|
|
405
431
|
// Create a unique key for the edge to avoid duplicates
|
|
406
432
|
const edgeKey = `${normalizedSource}→${normalizedTarget}`;
|
|
407
|
-
|
|
433
|
+
|
|
408
434
|
// Check if both source and target nodes exist in our architecture map
|
|
409
435
|
const sourceNodeExists = architectureMap.nodes.some(node => node.id === normalizedSource);
|
|
410
436
|
const targetNodeExists = architectureMap.nodes.some(node => node.id === normalizedTarget);
|
|
411
|
-
|
|
437
|
+
|
|
412
438
|
if (sourceNodeExists && targetNodeExists && !edgeSet.has(edgeKey)) {
|
|
413
439
|
edgeSet.add(edgeKey);
|
|
414
440
|
validEdges.push({
|
|
@@ -418,28 +444,28 @@ async function scan(directory) {
|
|
|
418
444
|
});
|
|
419
445
|
}
|
|
420
446
|
});
|
|
421
|
-
|
|
447
|
+
|
|
422
448
|
console.log('AFTER DEDUPLICATION EDGES:', validEdges.length);
|
|
423
|
-
|
|
449
|
+
|
|
424
450
|
// Replace the edges with only valid ones
|
|
425
451
|
architectureMap.edges = validEdges;
|
|
426
|
-
|
|
452
|
+
|
|
427
453
|
// Add top blast radius files to the architecture map
|
|
428
454
|
const totalFiles = architectureMap.nodes.length;
|
|
429
455
|
const { computeBlastRadiusWithPercentage, analyzeCriticality } = require('./blastRadius');
|
|
430
456
|
const allFilesWithPercentage = computeBlastRadiusWithPercentage(blastRadiusMap, totalFiles);
|
|
431
|
-
|
|
457
|
+
|
|
432
458
|
// Analyze criticality of files
|
|
433
459
|
const criticalityAnalysis = analyzeCriticality(blastRadiusMap, reverseGraph, architectureMap.nodes);
|
|
434
|
-
|
|
460
|
+
|
|
435
461
|
// Sort by blast radius descending and take top 3
|
|
436
462
|
const topFiles = allFilesWithPercentage.slice(0, 3);
|
|
437
|
-
|
|
463
|
+
|
|
438
464
|
architectureMap.contextSurface = {
|
|
439
465
|
topBlastRadiusFiles: topFiles,
|
|
440
466
|
criticalityAnalysis: criticalityAnalysis.slice(0, 5) // Top 5 most critical files
|
|
441
467
|
};
|
|
442
|
-
|
|
468
|
+
|
|
443
469
|
// Build the new context object that conforms to the schema
|
|
444
470
|
let context = buildContext(architectureMap.nodes, architectureMap.edges, {
|
|
445
471
|
directory: directory,
|
|
@@ -447,9 +473,9 @@ async function scan(directory) {
|
|
|
447
473
|
language: 'javascript',
|
|
448
474
|
contextSurface: architectureMap.contextSurface
|
|
449
475
|
});
|
|
450
|
-
|
|
476
|
+
|
|
451
477
|
console.log('Validating structural context...');
|
|
452
|
-
|
|
478
|
+
|
|
453
479
|
// Validate the context against the schema
|
|
454
480
|
let validation = validateContext(context);
|
|
455
481
|
|
|
@@ -458,7 +484,7 @@ async function scan(directory) {
|
|
|
458
484
|
validation.errors.forEach(e => console.error(' -', e));
|
|
459
485
|
|
|
460
486
|
console.log('Re-running generation deterministically...');
|
|
461
|
-
|
|
487
|
+
|
|
462
488
|
// Rebuild context deterministically
|
|
463
489
|
context = buildContext(architectureMap.nodes, architectureMap.edges, {
|
|
464
490
|
directory: directory,
|
|
@@ -476,12 +502,12 @@ async function scan(directory) {
|
|
|
476
502
|
process.exit(1);
|
|
477
503
|
}
|
|
478
504
|
}
|
|
479
|
-
|
|
505
|
+
|
|
480
506
|
// Sort the context deterministically
|
|
481
507
|
const sortedContext = sortContext(context);
|
|
482
|
-
|
|
508
|
+
|
|
483
509
|
console.log('STRUCTURAL CONTEXT VALIDATED — READY FOR UPLOAD');
|
|
484
|
-
|
|
510
|
+
|
|
485
511
|
return sortedContext;
|
|
486
512
|
|
|
487
513
|
} catch (err) {
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Builds cross-file symbol resolution and tracks actual usage of imported symbols.
|
|
5
|
+
* This enables detection of which files actually USE imported functions/classes,
|
|
6
|
+
* not just which files import them.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build a global symbol table from all file exports
|
|
11
|
+
* @param {Array} nodes - All scanned file nodes with metadata
|
|
12
|
+
* @returns {Map} Symbol table mapping symbol names to their source files
|
|
13
|
+
*/
|
|
14
|
+
function buildSymbolTable(nodes) {
|
|
15
|
+
const symbolTable = new Map();
|
|
16
|
+
|
|
17
|
+
nodes.forEach(node => {
|
|
18
|
+
const filePath = node.id;
|
|
19
|
+
const metadata = node.metadata;
|
|
20
|
+
|
|
21
|
+
if (!metadata) return;
|
|
22
|
+
|
|
23
|
+
// Register all exports from this file
|
|
24
|
+
if (metadata.exports && Array.isArray(metadata.exports)) {
|
|
25
|
+
metadata.exports.forEach(exp => {
|
|
26
|
+
const symbolName = exp.name;
|
|
27
|
+
if (!symbolTable.has(symbolName)) {
|
|
28
|
+
symbolTable.set(symbolName, []);
|
|
29
|
+
}
|
|
30
|
+
symbolTable.get(symbolName).push({
|
|
31
|
+
file: filePath,
|
|
32
|
+
type: exp.type,
|
|
33
|
+
loc: exp.loc
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Register all functions
|
|
39
|
+
if (metadata.functions && Array.isArray(metadata.functions)) {
|
|
40
|
+
metadata.functions.forEach(func => {
|
|
41
|
+
const symbolName = func.name;
|
|
42
|
+
if (!symbolTable.has(symbolName)) {
|
|
43
|
+
symbolTable.set(symbolName, []);
|
|
44
|
+
}
|
|
45
|
+
symbolTable.get(symbolName).push({
|
|
46
|
+
file: filePath,
|
|
47
|
+
type: 'function',
|
|
48
|
+
loc: func.loc
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Register all classes
|
|
54
|
+
if (metadata.classes && Array.isArray(metadata.classes)) {
|
|
55
|
+
metadata.classes.forEach(cls => {
|
|
56
|
+
const symbolName = cls.name;
|
|
57
|
+
if (!symbolTable.has(symbolName)) {
|
|
58
|
+
symbolTable.set(symbolName, []);
|
|
59
|
+
}
|
|
60
|
+
symbolTable.get(symbolName).push({
|
|
61
|
+
file: filePath,
|
|
62
|
+
type: 'class',
|
|
63
|
+
loc: cls.loc
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return symbolTable;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve symbol usage to their definitions
|
|
74
|
+
* @param {Object} node - File node with metadata
|
|
75
|
+
* @param {Map} symbolTable - Global symbol table
|
|
76
|
+
* @returns {Array} Array of resolved symbol usages
|
|
77
|
+
*/
|
|
78
|
+
function resolveSymbolUsage(node, symbolTable) {
|
|
79
|
+
const resolvedUsages = [];
|
|
80
|
+
const metadata = node.metadata;
|
|
81
|
+
|
|
82
|
+
if (!metadata) return resolvedUsages;
|
|
83
|
+
|
|
84
|
+
// Track which symbols this file imports
|
|
85
|
+
const importedSymbols = new Map();
|
|
86
|
+
if (metadata.imports && Array.isArray(metadata.imports)) {
|
|
87
|
+
metadata.imports.forEach(imp => {
|
|
88
|
+
if (imp.specifiers && Array.isArray(imp.specifiers)) {
|
|
89
|
+
imp.specifiers.forEach(spec => {
|
|
90
|
+
const localName = spec.local || spec.imported || spec;
|
|
91
|
+
const importedName = spec.imported || spec.local || spec;
|
|
92
|
+
importedSymbols.set(localName, {
|
|
93
|
+
source: imp.source,
|
|
94
|
+
originalName: importedName
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check function calls against imported symbols
|
|
102
|
+
if (metadata.functionCalls && Array.isArray(metadata.functionCalls)) {
|
|
103
|
+
metadata.functionCalls.forEach(call => {
|
|
104
|
+
const callName = call.name;
|
|
105
|
+
if (importedSymbols.has(callName)) {
|
|
106
|
+
const importInfo = importedSymbols.get(callName);
|
|
107
|
+
resolvedUsages.push({
|
|
108
|
+
type: 'function_call',
|
|
109
|
+
symbol: callName,
|
|
110
|
+
source: importInfo.source,
|
|
111
|
+
loc: call.loc
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check constructor calls against imported symbols
|
|
118
|
+
if (metadata.constructorCalls && Array.isArray(metadata.constructorCalls)) {
|
|
119
|
+
metadata.constructorCalls.forEach(call => {
|
|
120
|
+
const className = call.className;
|
|
121
|
+
if (importedSymbols.has(className)) {
|
|
122
|
+
const importInfo = importedSymbols.get(className);
|
|
123
|
+
resolvedUsages.push({
|
|
124
|
+
type: 'constructor_call',
|
|
125
|
+
symbol: className,
|
|
126
|
+
source: importInfo.source,
|
|
127
|
+
loc: call.loc
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check component usage (React/JSX)
|
|
134
|
+
if (metadata.componentUsage && Array.isArray(metadata.componentUsage)) {
|
|
135
|
+
metadata.componentUsage.forEach(comp => {
|
|
136
|
+
const componentName = comp.component;
|
|
137
|
+
if (importedSymbols.has(componentName)) {
|
|
138
|
+
const importInfo = importedSymbols.get(componentName);
|
|
139
|
+
resolvedUsages.push({
|
|
140
|
+
type: 'component_usage',
|
|
141
|
+
symbol: componentName,
|
|
142
|
+
source: importInfo.source,
|
|
143
|
+
loc: comp.loc
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return resolvedUsages;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Track cross-file references and build usage edges
|
|
154
|
+
* @param {Array} nodes - All scanned file nodes
|
|
155
|
+
* @param {Map} symbolTable - Global symbol table
|
|
156
|
+
* @returns {Array} Array of usage edges
|
|
157
|
+
*/
|
|
158
|
+
function trackCrossFileReferences(nodes, symbolTable) {
|
|
159
|
+
const usageEdges = [];
|
|
160
|
+
|
|
161
|
+
nodes.forEach(node => {
|
|
162
|
+
const filePath = node.id;
|
|
163
|
+
const resolvedUsages = resolveSymbolUsage(node, symbolTable);
|
|
164
|
+
|
|
165
|
+
resolvedUsages.forEach(usage => {
|
|
166
|
+
// Create an edge from this file to the source of the symbol
|
|
167
|
+
usageEdges.push({
|
|
168
|
+
source: filePath,
|
|
169
|
+
target: usage.source,
|
|
170
|
+
type: usage.type,
|
|
171
|
+
symbol: usage.symbol,
|
|
172
|
+
loc: usage.loc
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return usageEdges;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Analyze semantic relationships in the codebase
|
|
182
|
+
* @param {Array} nodes - All scanned file nodes
|
|
183
|
+
* @returns {Object} Semantic analysis results
|
|
184
|
+
*/
|
|
185
|
+
function analyzeSemantics(nodes) {
|
|
186
|
+
const symbolTable = buildSymbolTable(nodes);
|
|
187
|
+
const usageEdges = trackCrossFileReferences(nodes, symbolTable);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
symbolTable,
|
|
191
|
+
usageEdges,
|
|
192
|
+
stats: {
|
|
193
|
+
totalSymbols: symbolTable.size,
|
|
194
|
+
totalUsages: usageEdges.length
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
module.exports = {
|
|
200
|
+
buildSymbolTable,
|
|
201
|
+
resolveSymbolUsage,
|
|
202
|
+
trackCrossFileReferences,
|
|
203
|
+
analyzeSemantics
|
|
204
|
+
};
|