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,497 @@
1
+ // src/scanner/parsers/python.mjs
2
+ // Python parser with Django, FastAPI, Flask, Celery support
3
+
4
+ import { readFileSync, existsSync } from 'fs';
5
+
6
+ /**
7
+ * Parse a Python file and extract classes, functions, decorators, imports
8
+ * @param {Object|string} file - File object or path
9
+ * @returns {Object} - Parse result
10
+ */
11
+ export async function parse(file) {
12
+ const filePath = typeof file === 'string' ? file : file.path;
13
+ const relativePath = typeof file === 'string' ? file : file.relativePath;
14
+
15
+ if (!existsSync(filePath)) {
16
+ return createEmptyResult(filePath, relativePath, 'File not found');
17
+ }
18
+
19
+ let content;
20
+ try {
21
+ content = readFileSync(filePath, 'utf-8');
22
+ } catch (error) {
23
+ return createEmptyResult(filePath, relativePath, `Read error: ${error.message}`);
24
+ }
25
+
26
+ try {
27
+ const lines = content.split('\n');
28
+ const functions = [];
29
+ const classes = [];
30
+ const decorators = []; // All decorators found
31
+ const imports = [];
32
+
33
+ // Track decorators for next element
34
+ let pendingDecorators = [];
35
+
36
+ // Parse line by line
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const line = lines[i];
39
+ const lineNum = i + 1;
40
+
41
+ // Detect import statements
42
+ const importMatch = line.match(/^\s*import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
43
+ if (importMatch) {
44
+ imports.push({
45
+ module: importMatch[1],
46
+ alias: importMatch[2] || null,
47
+ type: 'import',
48
+ line: lineNum
49
+ });
50
+ continue;
51
+ }
52
+
53
+ // Detect from ... import statements (single-line and multi-line with parentheses)
54
+ const fromImportMatch = line.match(/^\s*from\s+([\w.]+)\s+import\s+(.+)/);
55
+ if (fromImportMatch) {
56
+ const module = fromImportMatch[1];
57
+ let importedText = fromImportMatch[2].trim();
58
+
59
+ // Handle multi-line parenthetical imports: from X import (\n item1,\n item2\n)
60
+ if (importedText.startsWith('(') && !importedText.includes(')')) {
61
+ // Collect lines until closing paren
62
+ importedText = importedText.slice(1); // remove opening paren
63
+ for (let j = i + 1; j < lines.length; j++) {
64
+ const nextLine = lines[j].trim();
65
+ if (nextLine.includes(')')) {
66
+ importedText += ',' + nextLine.replace(')', '');
67
+ i = j; // advance line pointer
68
+ break;
69
+ }
70
+ importedText += ',' + nextLine;
71
+ }
72
+ } else if (importedText.startsWith('(') && importedText.includes(')')) {
73
+ // Single-line parenthetical: from X import (a, b, c)
74
+ importedText = importedText.replace(/[()]/g, '');
75
+ }
76
+
77
+ const importedItems = importedText.split(',').map(s => s.trim()).filter(s => s && !s.startsWith('#'));
78
+
79
+ // If module is only dots (e.g. ".", ".."), imported items are submodules
80
+ // e.g. "from . import applications" → module should be ".applications"
81
+ // If module has a name after dots (e.g. ".applications"), imported items are symbols
82
+ // e.g. "from .applications import FastAPI" → module should be ".applications"
83
+ const isDotsOnly = /^\.+$/.test(module);
84
+
85
+ for (const item of importedItems) {
86
+ // Handle star import: from X import *
87
+ if (item.trim() === '*') {
88
+ imports.push({
89
+ module: module,
90
+ name: '*',
91
+ alias: null,
92
+ type: 'from',
93
+ line: lineNum
94
+ });
95
+ continue;
96
+ }
97
+ const aliasMatch = item.match(/(\w+)(?:\s+as\s+(\w+))?/);
98
+ if (aliasMatch) {
99
+ imports.push({
100
+ module: isDotsOnly ? `${module}${aliasMatch[1]}` : module,
101
+ name: aliasMatch[1],
102
+ alias: aliasMatch[2] || null,
103
+ type: 'from',
104
+ line: lineNum
105
+ });
106
+ }
107
+ }
108
+ continue;
109
+ }
110
+
111
+ // Detect decorators
112
+ const decoratorMatch = line.match(/^\s*@([\w.]+)(?:\(([^)]*)\))?/);
113
+ if (decoratorMatch) {
114
+ const decorator = {
115
+ name: decoratorMatch[1],
116
+ args: decoratorMatch[2] || null,
117
+ line: lineNum
118
+ };
119
+ decorators.push(decorator);
120
+ pendingDecorators.push(decorator);
121
+ continue;
122
+ }
123
+
124
+ // Detect class declaration
125
+ const classMatch = line.match(/^(\s*)class\s+(\w+)(?:\(([^)]*)\))?:/);
126
+ if (classMatch) {
127
+ const indent = classMatch[1].length;
128
+ const baseClasses = classMatch[3] ? classMatch[3].split(',').map(s => s.trim()) : [];
129
+
130
+ const classInfo = {
131
+ name: classMatch[2],
132
+ type: 'class',
133
+ line: lineNum,
134
+ endLine: findIndentBlockEnd(lines, i, indent),
135
+ indent,
136
+ baseClasses,
137
+ decorators: [...pendingDecorators],
138
+ methods: [],
139
+ exported: !classMatch[2].startsWith('_') // Not private
140
+ };
141
+
142
+ classInfo.lineCount = classInfo.endLine - classInfo.line + 1;
143
+ classInfo.sizeBytes = extractCode(content, classInfo.line, classInfo.endLine).length;
144
+
145
+ // Parse methods inside the class
146
+ parseClassMethods(lines, classInfo, functions);
147
+
148
+ classes.push(classInfo);
149
+ pendingDecorators = [];
150
+ continue;
151
+ }
152
+
153
+ // Detect function declaration (module-level)
154
+ const funcMatch = line.match(/^(\s*)(async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([\w\[\],\s.]+))?:/);
155
+ if (funcMatch && funcMatch[1].length === 0) { // Module-level only
156
+ const funcInfo = {
157
+ name: funcMatch[3],
158
+ type: funcMatch[2] ? 'async function' : 'function',
159
+ async: !!funcMatch[2],
160
+ line: lineNum,
161
+ endLine: findIndentBlockEnd(lines, i, 0),
162
+ params: parseParams(funcMatch[4]),
163
+ returnType: funcMatch[5]?.trim() || null,
164
+ decorators: [...pendingDecorators],
165
+ signature: `def ${funcMatch[3]}(${funcMatch[4]})`,
166
+ exported: !funcMatch[3].startsWith('_')
167
+ };
168
+
169
+ funcInfo.lineCount = funcInfo.endLine - funcInfo.line + 1;
170
+ funcInfo.sizeBytes = extractCode(content, funcInfo.line, funcInfo.endLine).length;
171
+
172
+ functions.push(funcInfo);
173
+ pendingDecorators = [];
174
+ continue;
175
+ }
176
+
177
+ // Clear pending decorators if we hit something else
178
+ if (line.trim() && !line.trim().startsWith('#')) {
179
+ pendingDecorators = [];
180
+ }
181
+ }
182
+
183
+ // Check for __main__ block
184
+ const hasMainBlock = content.includes('if __name__ == "__main__"') ||
185
+ content.includes("if __name__ == '__main__'");
186
+
187
+ // Determine exports (public functions and classes)
188
+ const exports = [
189
+ ...functions.filter(f => f.exported).map(f => ({
190
+ name: f.name,
191
+ type: 'function',
192
+ line: f.line
193
+ })),
194
+ ...classes.filter(c => c.exported).map(c => ({
195
+ name: c.name,
196
+ type: 'class',
197
+ line: c.line
198
+ }))
199
+ ];
200
+
201
+ // Check for __all__ definition
202
+ const allMatch = content.match(/__all__\s*=\s*\[([\s\S]*?)\]/);
203
+ if (allMatch) {
204
+ const exportNames = allMatch[1].match(/['"](\w+)['"]/g);
205
+ if (exportNames) {
206
+ // __all__ defines explicit exports
207
+ exports.length = 0;
208
+ for (const name of exportNames) {
209
+ const cleanName = name.replace(/['"]/g, '');
210
+ exports.push({
211
+ name: cleanName,
212
+ type: 'explicit',
213
+ line: 0
214
+ });
215
+ }
216
+ }
217
+ }
218
+
219
+ return {
220
+ file: { path: filePath, relativePath },
221
+ content,
222
+ functions,
223
+ classes,
224
+ exports,
225
+ imports,
226
+ annotations: decorators,
227
+ lines: lines.length,
228
+ size: content.length,
229
+ parseMethod: 'python-regex',
230
+ metadata: {
231
+ hasMainBlock,
232
+ isDjangoModel: classes.some(c =>
233
+ c.baseClasses.some(b => b.includes('Model') || b.includes('models.Model'))
234
+ ),
235
+ isDjangoView: classes.some(c =>
236
+ c.baseClasses.some(b => b.includes('View') || b.includes('APIView') || b.includes('ViewSet'))
237
+ ),
238
+ isFastAPI: decorators.some(d =>
239
+ d.name.includes('app.') || d.name.includes('router.') || d.name === 'Depends'
240
+ ),
241
+ isFlask: decorators.some(d =>
242
+ d.name.includes('route') || d.name.includes('Blueprint')
243
+ ),
244
+ isCelery: decorators.some(d =>
245
+ d.name === 'task' || d.name === 'shared_task' || d.name.includes('celery.')
246
+ )
247
+ }
248
+ };
249
+
250
+ } catch (error) {
251
+ return createEmptyResult(filePath, relativePath, `Parse error: ${error.message}`);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Parse methods inside a class
257
+ */
258
+ function parseClassMethods(lines, classInfo, allFunctions) {
259
+ const classIndent = classInfo.indent;
260
+
261
+ for (let i = classInfo.line; i < classInfo.endLine - 1 && i < lines.length; i++) {
262
+ const line = lines[i];
263
+ const lineNum = i + 1;
264
+
265
+ // Method declaration
266
+ const methodMatch = line.match(/^(\s+)(async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([\w\[\],\s.]+))?:/);
267
+ if (methodMatch) {
268
+ const methodIndent = methodMatch[1].length;
269
+ // Must be directly inside the class
270
+ if (methodIndent > classIndent) {
271
+ // Collect decorators from previous lines
272
+ const methodDecorators = [];
273
+ for (let j = i - 1; j >= classInfo.line; j--) {
274
+ const prevLine = lines[j];
275
+ const decMatch = prevLine.match(/^\s+@([\w.]+)(?:\(([^)]*)\))?/);
276
+ if (decMatch) {
277
+ methodDecorators.unshift({
278
+ name: decMatch[1],
279
+ args: decMatch[2] || null,
280
+ line: j + 1
281
+ });
282
+ } else if (prevLine.trim() && !prevLine.trim().startsWith('#')) {
283
+ break;
284
+ }
285
+ }
286
+
287
+ const methodInfo = {
288
+ name: methodMatch[3],
289
+ type: methodMatch[2] ? 'async method' : 'method',
290
+ async: !!methodMatch[2],
291
+ className: classInfo.name,
292
+ line: lineNum,
293
+ endLine: findIndentBlockEnd(lines, i, methodIndent),
294
+ params: parseParams(methodMatch[4]),
295
+ returnType: methodMatch[5]?.trim() || null,
296
+ decorators: methodDecorators,
297
+ signature: `def ${methodMatch[3]}(${methodMatch[4]})`,
298
+ isStatic: methodDecorators.some(d => d.name === 'staticmethod'),
299
+ isClassMethod: methodDecorators.some(d => d.name === 'classmethod'),
300
+ isProperty: methodDecorators.some(d => d.name === 'property')
301
+ };
302
+
303
+ methodInfo.lineCount = methodInfo.endLine - methodInfo.line + 1;
304
+
305
+ classInfo.methods.push(methodInfo);
306
+ allFunctions.push(methodInfo);
307
+ }
308
+ }
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Parse function parameters
314
+ */
315
+ function parseParams(paramsStr) {
316
+ if (!paramsStr || !paramsStr.trim()) return [];
317
+
318
+ const params = [];
319
+ let depth = 0;
320
+ let current = '';
321
+
322
+ for (const char of paramsStr) {
323
+ if (char === '[' || char === '(') depth++;
324
+ else if (char === ']' || char === ')') depth--;
325
+ else if (char === ',' && depth === 0) {
326
+ if (current.trim()) {
327
+ params.push(parseParam(current.trim()));
328
+ }
329
+ current = '';
330
+ continue;
331
+ }
332
+ current += char;
333
+ }
334
+
335
+ if (current.trim()) {
336
+ params.push(parseParam(current.trim()));
337
+ }
338
+
339
+ return params;
340
+ }
341
+
342
+ /**
343
+ * Parse a single parameter
344
+ */
345
+ function parseParam(paramStr) {
346
+ // Handle: name, name: type, name = default, name: type = default, *args, **kwargs
347
+ const match = paramStr.match(/^(\*{0,2})(\w+)(?:\s*:\s*([^=]+))?(?:\s*=\s*(.+))?$/);
348
+ if (match) {
349
+ return {
350
+ name: match[2],
351
+ type: match[3]?.trim() || null,
352
+ default: match[4]?.trim() || null,
353
+ isVararg: match[1] === '*',
354
+ isKwarg: match[1] === '**'
355
+ };
356
+ }
357
+ return { name: paramStr, type: null };
358
+ }
359
+
360
+ /**
361
+ * Find end of indented block
362
+ */
363
+ function findIndentBlockEnd(lines, startIndex, baseIndent) {
364
+ const firstLine = lines[startIndex];
365
+ const firstIndent = firstLine.match(/^(\s*)/)[1].length;
366
+
367
+ // If we're looking for module-level block end, use the function's indent
368
+ const targetIndent = baseIndent === 0 ? firstIndent : baseIndent;
369
+
370
+ for (let i = startIndex + 1; i < lines.length; i++) {
371
+ const line = lines[i];
372
+
373
+ // Skip empty lines and comments
374
+ if (!line.trim() || line.trim().startsWith('#')) {
375
+ continue;
376
+ }
377
+
378
+ // Check indent
379
+ const indent = line.match(/^(\s*)/)[1].length;
380
+
381
+ // If we find a line with less or equal indent (for non-module level)
382
+ // or less indent (for module level), block ends
383
+ if (baseIndent === 0) {
384
+ // Module-level function: ends when we see another module-level statement
385
+ if (indent === 0 && line.trim()) {
386
+ return i;
387
+ }
388
+ } else {
389
+ // Class/nested: ends when we see something at class level or less
390
+ if (indent <= targetIndent && line.trim()) {
391
+ return i;
392
+ }
393
+ }
394
+ }
395
+
396
+ return lines.length;
397
+ }
398
+
399
+ /**
400
+ * Extract code between line numbers
401
+ */
402
+ function extractCode(content, startLine, endLine) {
403
+ const lines = content.split('\n');
404
+ return lines.slice(startLine - 1, endLine).join('\n');
405
+ }
406
+
407
+ /**
408
+ * Create empty result
409
+ */
410
+ function createEmptyResult(filePath, relativePath, error) {
411
+ return {
412
+ file: { path: filePath, relativePath },
413
+ content: '',
414
+ functions: [],
415
+ classes: [],
416
+ exports: [],
417
+ imports: [],
418
+ annotations: [],
419
+ lines: 0,
420
+ size: 0,
421
+ error,
422
+ parseMethod: 'none'
423
+ };
424
+ }
425
+
426
+ /**
427
+ * Check if a Python class is a framework component
428
+ */
429
+ export function isFrameworkComponent(classInfo, parseResult) {
430
+ // Django Model
431
+ if (classInfo.baseClasses.some(b => b.includes('Model') || b.includes('models.Model'))) {
432
+ return { is: true, framework: 'django', type: 'model' };
433
+ }
434
+
435
+ // Django View
436
+ if (classInfo.baseClasses.some(b =>
437
+ b.includes('View') || b.includes('APIView') || b.includes('ViewSet') ||
438
+ b.includes('GenericAPIView') || b.includes('ModelViewSet')
439
+ )) {
440
+ return { is: true, framework: 'django', type: 'view' };
441
+ }
442
+
443
+ // Django Admin
444
+ if (classInfo.baseClasses.some(b => b.includes('ModelAdmin') || b.includes('admin.ModelAdmin'))) {
445
+ return { is: true, framework: 'django', type: 'admin' };
446
+ }
447
+
448
+ // Django Form
449
+ if (classInfo.baseClasses.some(b =>
450
+ b.includes('Form') || b.includes('ModelForm') || b.includes('forms.Form')
451
+ )) {
452
+ return { is: true, framework: 'django', type: 'form' };
453
+ }
454
+
455
+ // FastAPI/Flask router decorators on functions
456
+ for (const decorator of classInfo.decorators || []) {
457
+ if (decorator.name.includes('router.') || decorator.name.includes('app.')) {
458
+ return { is: true, framework: 'fastapi', type: 'route' };
459
+ }
460
+ }
461
+
462
+ return { is: false };
463
+ }
464
+
465
+ /**
466
+ * Check if a Python file is an entry point
467
+ */
468
+ export function isEntryPoint(parseResult) {
469
+ // Has if __name__ == "__main__"
470
+ if (parseResult.metadata?.hasMainBlock) {
471
+ return { isEntry: true, reason: 'Has __main__ block' };
472
+ }
473
+
474
+ // Django manage.py style
475
+ const fileName = parseResult.file?.relativePath || '';
476
+ if (fileName.endsWith('manage.py')) {
477
+ return { isEntry: true, reason: 'Is Django manage.py' };
478
+ }
479
+
480
+ // Django wsgi.py/asgi.py
481
+ if (fileName.endsWith('wsgi.py') || fileName.endsWith('asgi.py')) {
482
+ return { isEntry: true, reason: 'Is WSGI/ASGI entry point' };
483
+ }
484
+
485
+ // Celery tasks are entry points (executed by worker)
486
+ if (parseResult.metadata?.isCelery) {
487
+ return { isEntry: true, reason: 'Has Celery task decorators' };
488
+ }
489
+
490
+ return { isEntry: false };
491
+ }
492
+
493
+ export default {
494
+ parse,
495
+ isFrameworkComponent,
496
+ isEntryPoint
497
+ };