swynx-lite 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +113 -0
  2. package/bin/swynx-lite +3 -0
  3. package/package.json +47 -0
  4. package/src/clean.mjs +280 -0
  5. package/src/cli.mjs +264 -0
  6. package/src/config.mjs +121 -0
  7. package/src/output/console.mjs +298 -0
  8. package/src/output/json.mjs +76 -0
  9. package/src/output/progress.mjs +57 -0
  10. package/src/scan.mjs +143 -0
  11. package/src/security.mjs +62 -0
  12. package/src/shared/fixer/barrel-cleaner.mjs +192 -0
  13. package/src/shared/fixer/import-cleaner.mjs +237 -0
  14. package/src/shared/fixer/quarantine.mjs +218 -0
  15. package/src/shared/scanner/analysers/buildSystems.mjs +647 -0
  16. package/src/shared/scanner/analysers/configParsers.mjs +1086 -0
  17. package/src/shared/scanner/analysers/deadcode.mjs +6194 -0
  18. package/src/shared/scanner/analysers/entryPointDetector.mjs +634 -0
  19. package/src/shared/scanner/analysers/generatedCode.mjs +297 -0
  20. package/src/shared/scanner/analysers/imports.mjs +60 -0
  21. package/src/shared/scanner/discovery.mjs +240 -0
  22. package/src/shared/scanner/parse-worker.mjs +82 -0
  23. package/src/shared/scanner/parsers/assets.mjs +44 -0
  24. package/src/shared/scanner/parsers/csharp.mjs +400 -0
  25. package/src/shared/scanner/parsers/css.mjs +60 -0
  26. package/src/shared/scanner/parsers/go.mjs +445 -0
  27. package/src/shared/scanner/parsers/java.mjs +364 -0
  28. package/src/shared/scanner/parsers/javascript.mjs +823 -0
  29. package/src/shared/scanner/parsers/kotlin.mjs +350 -0
  30. package/src/shared/scanner/parsers/python.mjs +497 -0
  31. package/src/shared/scanner/parsers/registry.mjs +233 -0
  32. package/src/shared/scanner/parsers/rust.mjs +427 -0
  33. package/src/shared/scanner/scan-dead-code.mjs +316 -0
  34. package/src/shared/security/patterns.mjs +349 -0
  35. package/src/shared/security/proximity.mjs +84 -0
  36. package/src/shared/security/scanner.mjs +269 -0
@@ -0,0 +1,823 @@
1
+ // src/scanner/parsers/javascript.mjs
2
+ // Enterprise-grade JavaScript/TypeScript parser using Babel AST
3
+ // Captures every function, class, method, and code structure with exact boundaries
4
+
5
+ import { readFileSync, existsSync } from 'fs';
6
+ import { parse } from '@babel/parser';
7
+ import _traverse from '@babel/traverse';
8
+
9
+ // Handle both ESM and CJS default exports
10
+ const traverse = _traverse.default || _traverse;
11
+
12
+ /**
13
+ * Parse a JavaScript/TypeScript file with full AST analysis
14
+ * Captures every function, class, method with exact line numbers and sizes
15
+ */
16
+ export async function parseJavaScript(file) {
17
+ const filePath = typeof file === 'string' ? file : file.path;
18
+ const relativePath = typeof file === 'string' ? file : file.relativePath;
19
+
20
+ if (!existsSync(filePath)) {
21
+ return createEmptyResult(filePath, relativePath, 'File not found');
22
+ }
23
+
24
+ let content;
25
+ try {
26
+ content = readFileSync(filePath, 'utf-8');
27
+ } catch (error) {
28
+ return createEmptyResult(filePath, relativePath, `Read error: ${error.message}`);
29
+ }
30
+
31
+ // Handle Vue Single File Components (.vue)
32
+ // Extract script content from <script> or <script setup> block
33
+ let scriptContent = content;
34
+ let isVueSFC = false;
35
+ let scriptLineOffset = 0;
36
+
37
+ if (filePath.endsWith('.vue') || filePath.endsWith('.svelte')) {
38
+ isVueSFC = true;
39
+ const scriptMatch = content.match(/<script(?:\s+[^>]*)?>([\s\S]*?)<\/script>/i);
40
+ if (scriptMatch) {
41
+ scriptContent = scriptMatch[1];
42
+ // Calculate line offset to adjust reported line numbers
43
+ const beforeScript = content.slice(0, scriptMatch.index);
44
+ scriptLineOffset = (beforeScript.match(/\n/g) || []).length + 1;
45
+ } else {
46
+ // No script block found - return empty but valid result
47
+ return {
48
+ file: { path: filePath, relativePath },
49
+ content,
50
+ functions: [],
51
+ classes: [],
52
+ exports: [],
53
+ imports: [],
54
+ lines: content.split('\n').length,
55
+ size: content.length,
56
+ parseMethod: filePath.endsWith('.svelte') ? 'svelte-no-script' : 'vue-no-script'
57
+ };
58
+ }
59
+ }
60
+
61
+ const lines = content.split('\n');
62
+
63
+ try {
64
+ // Parse with Babel - supports JSX, TypeScript, and all modern syntax
65
+ const ast = parse(scriptContent, {
66
+ sourceType: 'unambiguous', // Auto-detect module vs script
67
+ plugins: [
68
+ 'jsx',
69
+ 'typescript',
70
+ 'decorators-legacy',
71
+ 'classProperties',
72
+ 'classPrivateProperties',
73
+ 'classPrivateMethods',
74
+ 'classStaticBlock',
75
+ 'exportDefaultFrom',
76
+ 'exportNamespaceFrom',
77
+ 'dynamicImport',
78
+ 'importMeta',
79
+ 'nullishCoalescingOperator',
80
+ 'optionalChaining',
81
+ 'optionalCatchBinding',
82
+ 'topLevelAwait',
83
+ 'asyncGenerators',
84
+ 'objectRestSpread',
85
+ 'numericSeparator',
86
+ 'bigInt',
87
+ 'throwExpressions',
88
+ 'regexpUnicodeSets', // ES2024 regex features (v flag)
89
+ 'importAttributes', // import assertions/attributes
90
+ 'explicitResourceManagement', // using declarations
91
+ 'sourcePhaseImports', // source imports
92
+ 'deferredImportEvaluation' // deferred imports
93
+ ],
94
+ errorRecovery: true, // Continue parsing on errors
95
+ allowReturnOutsideFunction: true,
96
+ allowSuperOutsideMethod: true,
97
+ allowUndeclaredExports: true
98
+ });
99
+
100
+ const functions = [];
101
+ const classes = [];
102
+ const exports = [];
103
+ const imports = [];
104
+
105
+ traverse(ast, {
106
+ // ═══════════════════════════════════════════════════════════════════
107
+ // FUNCTION DECLARATIONS: function name() {}
108
+ // ═══════════════════════════════════════════════════════════════════
109
+ FunctionDeclaration(path) {
110
+ if (!path.node.id) return; // Skip anonymous
111
+
112
+ const func = extractFunctionInfo(path.node, content, 'function');
113
+ func.name = path.node.id.name;
114
+ func.exported = isExported(path);
115
+ functions.push(func);
116
+ },
117
+
118
+ // ═══════════════════════════════════════════════════════════════════
119
+ // ARROW FUNCTIONS: const name = () => {} or const name = async () => {}
120
+ // ═══════════════════════════════════════════════════════════════════
121
+ VariableDeclarator(path) {
122
+ const init = path.node.init;
123
+ if (!init) return;
124
+ if (!path.node.id || path.node.id.type !== 'Identifier') return;
125
+
126
+ const name = path.node.id.name;
127
+
128
+ // Arrow function or function expression assigned to variable
129
+ if (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression') {
130
+ const func = extractFunctionInfo(init, content, init.type === 'ArrowFunctionExpression' ? 'arrow' : 'expression');
131
+ func.name = name;
132
+
133
+ // Get the full declaration including 'const name = '
134
+ const parent = path.parentPath?.node;
135
+ if (parent?.loc) {
136
+ func.line = parent.loc.start.line;
137
+ func.column = parent.loc.start.column;
138
+ }
139
+
140
+ // Check if exported
141
+ const grandParent = path.parentPath?.parentPath;
142
+ func.exported = grandParent?.node?.type === 'ExportNamedDeclaration';
143
+
144
+ // Recalculate size with full declaration
145
+ func.sizeBytes = extractCodeSize(content, func.line, func.endLine);
146
+
147
+ functions.push(func);
148
+ }
149
+ },
150
+
151
+ // ═══════════════════════════════════════════════════════════════════
152
+ // CLASS DECLARATIONS: class Name {}
153
+ // ═══════════════════════════════════════════════════════════════════
154
+ ClassDeclaration(path) {
155
+ const node = path.node;
156
+ if (!node.id) return;
157
+
158
+ const classInfo = {
159
+ name: node.id.name,
160
+ type: 'class',
161
+ line: node.loc?.start?.line || 0,
162
+ endLine: node.loc?.end?.line || 0,
163
+ column: node.loc?.start?.column || 0,
164
+ lineCount: 0,
165
+ sizeBytes: 0,
166
+ exported: isExported(path),
167
+ superClass: node.superClass?.name || null,
168
+ methods: [],
169
+ properties: [],
170
+ // Extract decorators for DI detection (@Service, @Injectable, etc.)
171
+ decorators: (node.decorators || []).map(dec => {
172
+ const expr = dec.expression;
173
+ let name = null;
174
+ let args = null;
175
+
176
+ if (expr.type === 'CallExpression') {
177
+ // @Service() or @Module.forRoot()
178
+ name = expr.callee?.name || expr.callee?.property?.name;
179
+ // Extract first argument if it's an object literal (for @Injectable({ providedIn: 'root' }))
180
+ if (expr.arguments?.[0]?.type === 'ObjectExpression') {
181
+ args = {};
182
+ for (const prop of expr.arguments[0].properties || []) {
183
+ if (prop.key?.name && prop.value) {
184
+ // Handle string literals and identifiers
185
+ if (prop.value.type === 'StringLiteral' || prop.value.type === 'Literal') {
186
+ args[prop.key.name] = prop.value.value;
187
+ } else if (prop.value.type === 'Identifier') {
188
+ args[prop.key.name] = prop.value.name;
189
+ }
190
+ }
191
+ }
192
+ }
193
+ } else if (expr.type === 'Identifier') {
194
+ // @Service (without parentheses)
195
+ name = expr.name;
196
+ } else if (expr.type === 'MemberExpression') {
197
+ // @Module.Service
198
+ name = expr.property?.name;
199
+ }
200
+
201
+ return { name, args, line: dec.loc?.start?.line || 0 };
202
+ }).filter(d => d.name)
203
+ };
204
+
205
+ classInfo.lineCount = classInfo.endLine - classInfo.line + 1;
206
+ classInfo.sizeBytes = extractCodeSize(content, classInfo.line, classInfo.endLine);
207
+
208
+ // Extract methods
209
+ if (node.body && node.body.body) {
210
+ for (const member of node.body.body) {
211
+ if (member.type === 'ClassMethod' || member.type === 'ClassPrivateMethod') {
212
+ const method = extractMethodInfo(member, content, classInfo.name);
213
+ classInfo.methods.push(method);
214
+ functions.push(method); // Also add to global functions for duplicate detection
215
+ } else if (member.type === 'ClassProperty' || member.type === 'ClassPrivateProperty') {
216
+ // Check if property is assigned a function
217
+ if (member.value?.type === 'ArrowFunctionExpression' ||
218
+ member.value?.type === 'FunctionExpression') {
219
+ const method = extractFunctionInfo(member.value, content, 'property');
220
+ method.name = member.key?.name || member.key?.id?.name || 'anonymous';
221
+ method.className = classInfo.name;
222
+ method.line = member.loc?.start?.line || 0;
223
+ method.endLine = member.loc?.end?.line || 0;
224
+ method.sizeBytes = extractCodeSize(content, method.line, method.endLine);
225
+ classInfo.methods.push(method);
226
+ functions.push(method);
227
+ } else {
228
+ classInfo.properties.push({
229
+ name: member.key?.name || 'unknown',
230
+ line: member.loc?.start?.line || 0,
231
+ static: member.static || false
232
+ });
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ classes.push(classInfo);
239
+ },
240
+
241
+ // ═══════════════════════════════════════════════════════════════════
242
+ // OBJECT METHODS: { methodName() {} } or { methodName: function() {} }
243
+ // ═══════════════════════════════════════════════════════════════════
244
+ ObjectMethod(path) {
245
+ if (!path.node.key) return;
246
+
247
+ const name = path.node.key.name || path.node.key.value || 'anonymous';
248
+ const func = extractFunctionInfo(path.node, content, 'method');
249
+ func.name = name;
250
+ func.isObjectMethod = true;
251
+
252
+ // Try to find parent object name
253
+ const parent = path.parentPath?.parentPath;
254
+ if (parent?.node?.type === 'VariableDeclarator' && parent.node.id?.name) {
255
+ func.objectName = parent.node.id.name;
256
+ }
257
+
258
+ functions.push(func);
259
+ },
260
+
261
+ ObjectProperty(path) {
262
+ const value = path.node.value;
263
+ if (!value) return;
264
+
265
+ // Property with function value: { name: function() {} } or { name: () => {} }
266
+ if (value.type === 'ArrowFunctionExpression' || value.type === 'FunctionExpression') {
267
+ const name = path.node.key?.name || path.node.key?.value || 'anonymous';
268
+ const func = extractFunctionInfo(value, content, 'property');
269
+ func.name = name;
270
+ func.isObjectProperty = true;
271
+
272
+ // Get full property bounds
273
+ func.line = path.node.loc?.start?.line || func.line;
274
+ func.endLine = path.node.loc?.end?.line || func.endLine;
275
+ func.sizeBytes = extractCodeSize(content, func.line, func.endLine);
276
+
277
+ functions.push(func);
278
+ }
279
+ },
280
+
281
+ // ═══════════════════════════════════════════════════════════════════
282
+ // IMPORTS
283
+ // ═══════════════════════════════════════════════════════════════════
284
+ ImportDeclaration(path) {
285
+ const node = path.node;
286
+ const source = node.source?.value;
287
+ if (!source) return;
288
+
289
+ const importInfo = {
290
+ module: source,
291
+ line: node.loc?.start?.line || 0,
292
+ type: 'esm',
293
+ specifiers: []
294
+ };
295
+
296
+ for (const spec of node.specifiers || []) {
297
+ if (spec.type === 'ImportDefaultSpecifier') {
298
+ importInfo.specifiers.push({
299
+ name: spec.local?.name,
300
+ type: 'default'
301
+ });
302
+ } else if (spec.type === 'ImportNamespaceSpecifier') {
303
+ importInfo.specifiers.push({
304
+ name: spec.local?.name,
305
+ type: 'namespace'
306
+ });
307
+ } else if (spec.type === 'ImportSpecifier') {
308
+ importInfo.specifiers.push({
309
+ name: spec.imported?.name || spec.local?.name,
310
+ localName: spec.local?.name,
311
+ type: 'named'
312
+ });
313
+ }
314
+ }
315
+
316
+ imports.push(importInfo);
317
+ },
318
+
319
+ // Handle dynamic imports: import('./module')
320
+ Import(path) {
321
+ // The parent is a CallExpression with the dynamic import
322
+ const parent = path.parentPath;
323
+ if (parent?.node?.type === 'CallExpression') {
324
+ const arg = parent.node.arguments?.[0];
325
+ if (arg?.type === 'StringLiteral' || arg?.type === 'Literal') {
326
+ const modulePath = arg.value;
327
+ if (modulePath) {
328
+ imports.push({
329
+ module: modulePath,
330
+ line: parent.node.loc?.start?.line || 0,
331
+ type: 'dynamic-import',
332
+ isDynamic: true
333
+ });
334
+ }
335
+ }
336
+ }
337
+ },
338
+
339
+ // Handle require() calls and dynamic module loading patterns
340
+ CallExpression(path) {
341
+ const node = path.node;
342
+
343
+ // Handle dynamic import() as CallExpression (older parser versions)
344
+ if (node.callee?.type === 'Import' && node.arguments?.[0]) {
345
+ const arg = node.arguments[0];
346
+ const modulePath = arg.value || arg.quasis?.[0]?.value?.raw;
347
+ if (modulePath && typeof modulePath === 'string') {
348
+ imports.push({
349
+ module: modulePath,
350
+ line: node.loc?.start?.line || 0,
351
+ type: 'dynamic-import',
352
+ isDynamic: true
353
+ });
354
+ }
355
+ }
356
+
357
+ // Handle require('module')
358
+ if (node.callee?.name === 'require' && node.arguments?.[0]?.value) {
359
+ imports.push({
360
+ module: node.arguments[0].value,
361
+ line: node.loc?.start?.line || 0,
362
+ type: 'commonjs'
363
+ });
364
+ }
365
+
366
+ // Handle glob.sync('**/*.node.ts') - Node.js glob
367
+ if (node.callee?.type === 'MemberExpression' &&
368
+ node.callee.object?.name === 'glob' &&
369
+ node.callee.property?.name === 'sync') {
370
+ const pattern = node.arguments?.[0]?.value;
371
+ if (pattern && typeof pattern === 'string') {
372
+ imports.push({
373
+ module: pattern,
374
+ line: node.loc?.start?.line || 0,
375
+ type: 'glob-sync',
376
+ isGlob: true
377
+ });
378
+ }
379
+ }
380
+
381
+ // Handle globSync('**/*.ts') - glob v9+ named export
382
+ if (node.callee?.name === 'globSync' && node.arguments?.[0]?.value) {
383
+ const pattern = node.arguments[0].value;
384
+ if (typeof pattern === 'string') {
385
+ imports.push({
386
+ module: pattern,
387
+ line: node.loc?.start?.line || 0,
388
+ type: 'glob-sync',
389
+ isGlob: true
390
+ });
391
+ }
392
+ }
393
+
394
+ // Handle import.meta.glob('**/*.ts') - Vite
395
+ if (node.callee?.type === 'MemberExpression' &&
396
+ node.callee.object?.type === 'MetaProperty' &&
397
+ node.callee.property?.name === 'glob') {
398
+ const pattern = node.arguments?.[0]?.value;
399
+ if (pattern && typeof pattern === 'string') {
400
+ imports.push({
401
+ module: pattern,
402
+ line: node.loc?.start?.line || 0,
403
+ type: 'import-meta-glob',
404
+ isGlob: true
405
+ });
406
+ }
407
+ }
408
+
409
+ // Handle require.context('./', true, /\.ts$/) - Webpack
410
+ if (node.callee?.type === 'MemberExpression' &&
411
+ node.callee.object?.name === 'require' &&
412
+ node.callee.property?.name === 'context') {
413
+ const dir = node.arguments?.[0]?.value;
414
+ const regexNode = node.arguments?.[2];
415
+ if (dir) {
416
+ imports.push({
417
+ module: dir,
418
+ line: node.loc?.start?.line || 0,
419
+ type: 'require-context',
420
+ isGlob: true,
421
+ recursive: node.arguments?.[1]?.value ?? false,
422
+ pattern: regexNode?.regex?.pattern || regexNode?.pattern || '.*'
423
+ });
424
+ }
425
+ }
426
+ },
427
+
428
+ // ═══════════════════════════════════════════════════════════════════
429
+ // EXPORTS
430
+ // ═══════════════════════════════════════════════════════════════════
431
+ ExportNamedDeclaration(path) {
432
+ const node = path.node;
433
+ const decl = node.declaration;
434
+
435
+ if (decl) {
436
+ if (decl.type === 'FunctionDeclaration' && decl.id) {
437
+ exports.push({
438
+ name: decl.id.name,
439
+ type: 'function',
440
+ line: node.loc?.start?.line || 0
441
+ });
442
+ } else if (decl.type === 'VariableDeclaration') {
443
+ for (const d of decl.declarations) {
444
+ if (d.id?.name) {
445
+ exports.push({
446
+ name: d.id.name,
447
+ type: 'variable',
448
+ line: node.loc?.start?.line || 0
449
+ });
450
+ }
451
+ }
452
+ } else if (decl.type === 'ClassDeclaration' && decl.id) {
453
+ exports.push({
454
+ name: decl.id.name,
455
+ type: 'class',
456
+ line: node.loc?.start?.line || 0
457
+ });
458
+ }
459
+ }
460
+
461
+ // export { foo, bar } or export { foo } from './module'
462
+ for (const spec of node.specifiers || []) {
463
+ exports.push({
464
+ name: spec.exported?.name || spec.local?.name,
465
+ type: 'reexport',
466
+ line: node.loc?.start?.line || 0,
467
+ sourceModule: node.source?.value || null // Capture re-export source for barrel files
468
+ });
469
+ }
470
+ },
471
+
472
+ // ═══════════════════════════════════════════════════════════════════
473
+ // EXPORT ALL: export * from './module'
474
+ // ═══════════════════════════════════════════════════════════════════
475
+ ExportAllDeclaration(path) {
476
+ exports.push({
477
+ name: '*',
478
+ type: 'reexport-all',
479
+ sourceModule: path.node.source?.value || null,
480
+ line: path.node.loc?.start?.line || 0
481
+ });
482
+ },
483
+
484
+ ExportDefaultDeclaration(path) {
485
+ const node = path.node;
486
+ let name = 'default';
487
+
488
+ if (node.declaration) {
489
+ if (node.declaration.id?.name) {
490
+ name = node.declaration.id.name;
491
+ } else if (node.declaration.type === 'Identifier') {
492
+ name = node.declaration.name;
493
+ }
494
+ }
495
+
496
+ exports.push({
497
+ name,
498
+ type: 'default',
499
+ isDefault: true,
500
+ line: node.loc?.start?.line || 0
501
+ });
502
+ }
503
+ });
504
+
505
+ // Sort functions by line number
506
+ functions.sort((a, b) => a.line - b.line);
507
+ classes.sort((a, b) => a.line - b.line);
508
+
509
+ return {
510
+ file: { path: filePath, relativePath },
511
+ content,
512
+ functions,
513
+ classes,
514
+ exports,
515
+ imports,
516
+ lines: lines.length,
517
+ size: content.length,
518
+ parseMethod: isVueSFC ? 'babel-ast-vue' : 'babel-ast',
519
+ ...(isVueSFC && { scriptLineOffset }) // Include offset for Vue SFCs
520
+ };
521
+
522
+ } catch (parseError) {
523
+ // Fallback to regex parsing for files Babel can't handle
524
+ console.warn(`[Parser] Babel failed for ${relativePath}, using regex fallback: ${parseError.message}`);
525
+ return parseWithRegex(filePath, relativePath, content, lines);
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Extract function information from AST node
531
+ */
532
+ function extractFunctionInfo(node, content, type) {
533
+ const loc = node.loc || {};
534
+ const startLine = loc.start?.line || 0;
535
+ const endLine = loc.end?.line || 0;
536
+ const startColumn = loc.start?.column || 0;
537
+
538
+ const info = {
539
+ name: node.id?.name || 'anonymous',
540
+ type,
541
+ line: startLine,
542
+ endLine,
543
+ column: startColumn,
544
+ lineCount: endLine - startLine + 1,
545
+ sizeBytes: 0,
546
+ async: node.async || false,
547
+ generator: node.generator || false,
548
+ params: [],
549
+ signature: ''
550
+ };
551
+
552
+ // Extract parameters
553
+ for (const param of node.params || []) {
554
+ if (param.type === 'Identifier') {
555
+ info.params.push(param.name);
556
+ } else if (param.type === 'AssignmentPattern' && param.left?.name) {
557
+ info.params.push(`${param.left.name}=`);
558
+ } else if (param.type === 'RestElement' && param.argument?.name) {
559
+ info.params.push(`...${param.argument.name}`);
560
+ } else if (param.type === 'ObjectPattern') {
561
+ info.params.push('{...}');
562
+ } else if (param.type === 'ArrayPattern') {
563
+ info.params.push('[...]');
564
+ }
565
+ }
566
+
567
+ // Build signature
568
+ const asyncPrefix = info.async ? 'async ' : '';
569
+ const genPrefix = info.generator ? '*' : '';
570
+ info.signature = `${asyncPrefix}function${genPrefix} ${info.name}(${info.params.join(', ')})`;
571
+
572
+ // Compute size without storing full body (saves ~5GB on large repos)
573
+ info.sizeBytes = extractCodeSize(content, startLine, endLine);
574
+
575
+ return info;
576
+ }
577
+
578
+ /**
579
+ * Extract class method information
580
+ */
581
+ function extractMethodInfo(node, content, className) {
582
+ const loc = node.loc || {};
583
+ const startLine = loc.start?.line || 0;
584
+ const endLine = loc.end?.line || 0;
585
+
586
+ let name = 'anonymous';
587
+ if (node.key) {
588
+ if (node.key.type === 'Identifier') {
589
+ name = node.key.name;
590
+ } else if (node.key.type === 'PrivateName') {
591
+ name = `#${node.key.id?.name || 'private'}`;
592
+ }
593
+ }
594
+
595
+ const info = {
596
+ name,
597
+ type: 'method',
598
+ kind: node.kind || 'method', // 'constructor', 'method', 'get', 'set'
599
+ className,
600
+ line: startLine,
601
+ endLine,
602
+ column: loc.start?.column || 0,
603
+ lineCount: endLine - startLine + 1,
604
+ sizeBytes: 0,
605
+ async: node.async || false,
606
+ generator: node.generator || false,
607
+ static: node.static || false,
608
+ params: [],
609
+ signature: ''
610
+ };
611
+
612
+ // Extract parameters
613
+ for (const param of node.params || []) {
614
+ if (param.type === 'Identifier') {
615
+ info.params.push(param.name);
616
+ } else if (param.type === 'AssignmentPattern' && param.left?.name) {
617
+ info.params.push(`${param.left.name}=`);
618
+ } else if (param.type === 'RestElement' && param.argument?.name) {
619
+ info.params.push(`...${param.argument.name}`);
620
+ }
621
+ }
622
+
623
+ // Build signature
624
+ const staticPrefix = info.static ? 'static ' : '';
625
+ const asyncPrefix = info.async ? 'async ' : '';
626
+ info.signature = `${staticPrefix}${asyncPrefix}${name}(${info.params.join(', ')})`;
627
+
628
+ // Compute size without storing full body (saves ~5GB on large repos)
629
+ info.sizeBytes = extractCodeSize(content, startLine, endLine);
630
+
631
+ return info;
632
+ }
633
+
634
+ /**
635
+ * Check if a node is exported
636
+ */
637
+ function isExported(path) {
638
+ const parent = path.parentPath;
639
+ if (!parent) return false;
640
+
641
+ return parent.node?.type === 'ExportNamedDeclaration' ||
642
+ parent.node?.type === 'ExportDefaultDeclaration';
643
+ }
644
+
645
+ /**
646
+ * Extract code between line numbers
647
+ */
648
+ function extractCode(content, startLine, endLine) {
649
+ if (!startLine || !endLine) return '';
650
+
651
+ const lines = content.split('\n');
652
+ const start = Math.max(0, startLine - 1);
653
+ const end = Math.min(lines.length, endLine);
654
+
655
+ return lines.slice(start, end).join('\n');
656
+ }
657
+
658
+ /**
659
+ * Calculate code size between line numbers
660
+ */
661
+ function extractCodeSize(content, startLine, endLine) {
662
+ return extractCode(content, startLine, endLine).length;
663
+ }
664
+
665
+ /**
666
+ * Create empty result for error cases
667
+ */
668
+ function createEmptyResult(filePath, relativePath, error) {
669
+ return {
670
+ file: { path: filePath, relativePath },
671
+ content: '',
672
+ functions: [],
673
+ classes: [],
674
+ exports: [],
675
+ imports: [],
676
+ lines: 0,
677
+ size: 0,
678
+ error,
679
+ parseMethod: 'none'
680
+ };
681
+ }
682
+
683
+ /**
684
+ * Fallback regex-based parsing for files Babel can't handle
685
+ */
686
+ function parseWithRegex(filePath, relativePath, content, lines) {
687
+ const functions = [];
688
+ const classes = [];
689
+ const exports = [];
690
+ const imports = [];
691
+
692
+ // Track brace depth for finding function boundaries
693
+ const functionPatterns = [
694
+ /^(\s*)(export\s+)?(async\s+)?function\s*\*?\s*(\w+)\s*\(/,
695
+ /^(\s*)(export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s+)?\([^)]*\)\s*=>/,
696
+ /^(\s*)(export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s+)?(\w+)\s*=>/,
697
+ /^(\s*)(export\s+)?class\s+(\w+)/
698
+ ];
699
+
700
+ for (let i = 0; i < lines.length; i++) {
701
+ const line = lines[i];
702
+
703
+ // Check for function patterns
704
+ for (const pattern of functionPatterns) {
705
+ const match = line.match(pattern);
706
+ if (match) {
707
+ const startLine = i + 1;
708
+ const endLine = findBlockEnd(lines, i);
709
+ const name = match[4] || match[3] || 'anonymous';
710
+ const body = lines.slice(i, endLine).join('\n');
711
+
712
+ if (pattern.source.includes('class')) {
713
+ classes.push({
714
+ name,
715
+ type: 'class',
716
+ line: startLine,
717
+ endLine,
718
+ lineCount: endLine - startLine + 1,
719
+ sizeBytes: body.length
720
+ });
721
+ } else {
722
+ functions.push({
723
+ name,
724
+ type: 'function',
725
+ line: startLine,
726
+ endLine,
727
+ lineCount: endLine - startLine + 1,
728
+ sizeBytes: body.length,
729
+ signature: line.trim().replace(/\{.*$/, '').trim()
730
+ });
731
+ }
732
+ break;
733
+ }
734
+ }
735
+
736
+ // Check for imports
737
+ const importMatch = line.match(/^import\s+.*from\s+['"]([^'"]+)['"]/);
738
+ if (importMatch) {
739
+ imports.push({ module: importMatch[1], line: i + 1, type: 'esm' });
740
+ }
741
+
742
+ const requireMatch = line.match(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
743
+ if (requireMatch) {
744
+ imports.push({ module: requireMatch[1], line: i + 1, type: 'commonjs' });
745
+ }
746
+
747
+ // Check for exports
748
+ if (/^export\s+(default\s+)?/.test(line)) {
749
+ const exportMatch = line.match(/export\s+(default\s+)?(function|class|const|let|var)?\s*(\w+)?/);
750
+ if (exportMatch) {
751
+ exports.push({
752
+ name: exportMatch[3] || 'default',
753
+ type: exportMatch[2] || 'default',
754
+ isDefault: !!exportMatch[1],
755
+ line: i + 1
756
+ });
757
+ }
758
+ }
759
+ }
760
+
761
+ return {
762
+ file: { path: filePath, relativePath },
763
+ content,
764
+ functions,
765
+ classes,
766
+ exports,
767
+ imports,
768
+ lines: lines.length,
769
+ size: content.length,
770
+ parseMethod: 'regex-fallback'
771
+ };
772
+ }
773
+
774
+ /**
775
+ * Find the end of a code block by tracking braces
776
+ */
777
+ function findBlockEnd(lines, startIndex) {
778
+ let braceDepth = 0;
779
+ let started = false;
780
+
781
+ for (let i = startIndex; i < lines.length; i++) {
782
+ const line = lines[i];
783
+
784
+ // Skip strings and comments (simplified)
785
+ let inString = false;
786
+ let stringChar = '';
787
+
788
+ for (let j = 0; j < line.length; j++) {
789
+ const char = line[j];
790
+ const nextChar = line[j + 1];
791
+
792
+ // Handle strings
793
+ if (!inString && (char === '"' || char === "'" || char === '`')) {
794
+ inString = true;
795
+ stringChar = char;
796
+ continue;
797
+ }
798
+ if (inString && char === stringChar && line[j - 1] !== '\\') {
799
+ inString = false;
800
+ continue;
801
+ }
802
+ if (inString) continue;
803
+
804
+ // Handle single-line comments
805
+ if (char === '/' && nextChar === '/') break;
806
+
807
+ // Count braces
808
+ if (char === '{') {
809
+ braceDepth++;
810
+ started = true;
811
+ } else if (char === '}') {
812
+ braceDepth--;
813
+ if (started && braceDepth === 0) {
814
+ return i + 1;
815
+ }
816
+ }
817
+ }
818
+ }
819
+
820
+ return startIndex + 1;
821
+ }
822
+
823
+ export default { parseJavaScript };