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.
- package/README.md +113 -0
- package/bin/swynx-lite +3 -0
- package/package.json +47 -0
- package/src/clean.mjs +280 -0
- package/src/cli.mjs +264 -0
- package/src/config.mjs +121 -0
- package/src/output/console.mjs +298 -0
- package/src/output/json.mjs +76 -0
- package/src/output/progress.mjs +57 -0
- package/src/scan.mjs +143 -0
- package/src/security.mjs +62 -0
- package/src/shared/fixer/barrel-cleaner.mjs +192 -0
- package/src/shared/fixer/import-cleaner.mjs +237 -0
- package/src/shared/fixer/quarantine.mjs +218 -0
- package/src/shared/scanner/analysers/buildSystems.mjs +647 -0
- package/src/shared/scanner/analysers/configParsers.mjs +1086 -0
- package/src/shared/scanner/analysers/deadcode.mjs +6194 -0
- package/src/shared/scanner/analysers/entryPointDetector.mjs +634 -0
- package/src/shared/scanner/analysers/generatedCode.mjs +297 -0
- package/src/shared/scanner/analysers/imports.mjs +60 -0
- package/src/shared/scanner/discovery.mjs +240 -0
- package/src/shared/scanner/parse-worker.mjs +82 -0
- package/src/shared/scanner/parsers/assets.mjs +44 -0
- package/src/shared/scanner/parsers/csharp.mjs +400 -0
- package/src/shared/scanner/parsers/css.mjs +60 -0
- package/src/shared/scanner/parsers/go.mjs +445 -0
- package/src/shared/scanner/parsers/java.mjs +364 -0
- package/src/shared/scanner/parsers/javascript.mjs +823 -0
- package/src/shared/scanner/parsers/kotlin.mjs +350 -0
- package/src/shared/scanner/parsers/python.mjs +497 -0
- package/src/shared/scanner/parsers/registry.mjs +233 -0
- package/src/shared/scanner/parsers/rust.mjs +427 -0
- package/src/shared/scanner/scan-dead-code.mjs +316 -0
- package/src/shared/security/patterns.mjs +349 -0
- package/src/shared/security/proximity.mjs +84 -0
- 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 };
|