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,44 @@
1
+ // src/scanner/parsers/assets.mjs
2
+ // Asset file analyser
3
+
4
+ import { statSync, existsSync } from 'fs';
5
+ import { extname, basename } from 'path';
6
+
7
+ /**
8
+ * Analyse an asset file
9
+ */
10
+ export async function analyseAssets(file) {
11
+ const filePath = typeof file === 'string' ? file : file.path;
12
+ const relativePath = typeof file === 'string' ? file : file.relativePath;
13
+
14
+ if (!existsSync(filePath)) {
15
+ return {
16
+ file: { path: filePath, relativePath },
17
+ type: 'unknown',
18
+ size: 0
19
+ };
20
+ }
21
+
22
+ const ext = extname(filePath).toLowerCase();
23
+ const stats = statSync(filePath);
24
+
25
+ let type = 'other';
26
+ if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico'].includes(ext)) {
27
+ type = 'image';
28
+ } else if (['.woff', '.woff2', '.ttf', '.eot', '.otf'].includes(ext)) {
29
+ type = 'font';
30
+ } else if (['.mp4', '.webm', '.ogg', '.mp3', '.wav'].includes(ext)) {
31
+ type = 'media';
32
+ }
33
+
34
+ return {
35
+ file: { path: filePath, relativePath },
36
+ name: basename(filePath),
37
+ type,
38
+ ext,
39
+ size: stats.size,
40
+ sizeBytes: stats.size
41
+ };
42
+ }
43
+
44
+ export default { analyseAssets };
@@ -0,0 +1,400 @@
1
+ // src/scanner/parsers/csharp.mjs
2
+ // C#/.NET parser with ASP.NET Core, Entity Framework support
3
+
4
+ import { readFileSync, existsSync } from 'fs';
5
+
6
+ /**
7
+ * Parse a C# file and extract classes, methods, attributes, usings
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 annotations = []; // Called 'attributes' in C#
31
+ const imports = []; // Called 'usings' in C#
32
+ let namespace = null;
33
+
34
+ // Extract namespace
35
+ const namespaceMatch = content.match(/^\s*namespace\s+([\w.]+)/m);
36
+ if (namespaceMatch) {
37
+ namespace = namespaceMatch[1];
38
+ }
39
+
40
+ // File-scoped namespace (C# 10+)
41
+ const fileScopedNsMatch = content.match(/^\s*namespace\s+([\w.]+)\s*;/m);
42
+ if (fileScopedNsMatch) {
43
+ namespace = fileScopedNsMatch[1];
44
+ }
45
+
46
+ // Extract using statements
47
+ const usingPattern = /^\s*using\s+(static\s+)?(global\s+)?([\w.]+)(?:\s*=\s*([\w.]+))?\s*;/gm;
48
+ let usingMatch;
49
+ while ((usingMatch = usingPattern.exec(content)) !== null) {
50
+ const lineNum = content.substring(0, usingMatch.index).split('\n').length;
51
+ imports.push({
52
+ module: usingMatch[3],
53
+ alias: usingMatch[4] || null,
54
+ type: usingMatch[1] ? 'static' : (usingMatch[2] ? 'global' : 'normal'),
55
+ line: lineNum
56
+ });
57
+ }
58
+
59
+ // Track attributes on current element
60
+ let pendingAttributes = [];
61
+
62
+ // Parse line by line
63
+ for (let i = 0; i < lines.length; i++) {
64
+ const line = lines[i];
65
+ const lineNum = i + 1;
66
+
67
+ // Detect attributes [Attribute] or [Attribute(args)]
68
+ const attributePattern = /\[(\w+)(?:\s*\(([^)\]]*)\))?\]/g;
69
+ let attrMatch;
70
+ while ((attrMatch = attributePattern.exec(line)) !== null) {
71
+ const attr = {
72
+ name: attrMatch[1],
73
+ args: attrMatch[2] || null,
74
+ line: lineNum
75
+ };
76
+ annotations.push(attr);
77
+ pendingAttributes.push(attr);
78
+ }
79
+
80
+ // Detect class/struct/interface/record declaration
81
+ const classMatch = line.match(/^\s*(public|private|protected|internal)?\s*(sealed|abstract|static|partial)?\s*(class|struct|interface|record)\s+(\w+)(?:<[^>]+>)?(?:\s*:\s*([\w<>,\s]+))?/);
82
+ if (classMatch) {
83
+ const baseTypes = classMatch[5] ? classMatch[5].split(',').map(s => s.trim()) : [];
84
+
85
+ const classInfo = {
86
+ name: classMatch[4],
87
+ type: classMatch[3], // class, struct, interface, record
88
+ visibility: classMatch[1] || 'internal',
89
+ modifiers: classMatch[2] ? classMatch[2].split(/\s+/).filter(m => m) : [],
90
+ line: lineNum,
91
+ endLine: findBlockEnd(lines, i),
92
+ baseTypes,
93
+ decorators: [...pendingAttributes],
94
+ attributes: [...pendingAttributes],
95
+ methods: [],
96
+ properties: [],
97
+ exported: ['public', 'protected', 'internal'].includes(classMatch[1])
98
+ };
99
+
100
+ classInfo.lineCount = classInfo.endLine - classInfo.line + 1;
101
+ classInfo.sizeBytes = extractCode(content, classInfo.line, classInfo.endLine).length;
102
+
103
+ classes.push(classInfo);
104
+ pendingAttributes = [];
105
+ }
106
+
107
+ // Detect method declaration
108
+ const methodMatch = line.match(/^\s*(public|private|protected|internal)?\s*(static|virtual|override|abstract|async|sealed)?\s*(?:([\w<>[\],?\s]+)\s+)?(\w+)\s*\(([^)]*)\)/);
109
+ if (methodMatch && !line.includes(' class ') && !line.includes(' struct ') && !line.includes(' new ') && !line.includes(' => ')) {
110
+ const methodInfo = {
111
+ name: methodMatch[4],
112
+ type: 'method',
113
+ visibility: methodMatch[1] || 'private',
114
+ modifiers: methodMatch[2] ? [methodMatch[2]] : [],
115
+ returnType: methodMatch[3]?.trim() || 'void',
116
+ params: parseParams(methodMatch[5]),
117
+ line: lineNum,
118
+ endLine: findMethodEnd(lines, i),
119
+ decorators: [...pendingAttributes],
120
+ attributes: [...pendingAttributes],
121
+ signature: `${methodMatch[3] || 'void'} ${methodMatch[4]}(${methodMatch[5]})`
122
+ };
123
+
124
+ // Check for Main method
125
+ if (methodMatch[4] === 'Main' && (methodMatch[2] === 'static' || line.includes('static'))) {
126
+ methodInfo.isMainMethod = true;
127
+ }
128
+
129
+ // Check for async Main
130
+ if (methodMatch[4] === 'Main' && methodMatch[2] === 'async') {
131
+ methodInfo.isMainMethod = true;
132
+ }
133
+
134
+ functions.push(methodInfo);
135
+
136
+ // Add to current class
137
+ if (classes.length > 0) {
138
+ const currentClass = classes[classes.length - 1];
139
+ if (lineNum > currentClass.line && lineNum < currentClass.endLine) {
140
+ currentClass.methods.push(methodInfo);
141
+ }
142
+ }
143
+
144
+ pendingAttributes = [];
145
+ }
146
+
147
+ // Detect properties
148
+ const propMatch = line.match(/^\s*(public|private|protected|internal)?\s*(static|virtual|override|abstract)?\s*([\w<>[\],?\s]+)\s+(\w+)\s*\{/);
149
+ if (propMatch && !line.includes('(') && !line.includes(' class ')) {
150
+ const propInfo = {
151
+ name: propMatch[4],
152
+ type: propMatch[3]?.trim(),
153
+ visibility: propMatch[1] || 'private',
154
+ line: lineNum,
155
+ attributes: [...pendingAttributes]
156
+ };
157
+
158
+ if (classes.length > 0) {
159
+ const currentClass = classes[classes.length - 1];
160
+ if (lineNum > currentClass.line && lineNum < currentClass.endLine) {
161
+ currentClass.properties.push(propInfo);
162
+ }
163
+ }
164
+
165
+ pendingAttributes = [];
166
+ }
167
+
168
+ // Clear pending attributes
169
+ if (line.trim() && !line.trim().startsWith('[') && !line.trim().startsWith('//') && !line.trim().startsWith('/*')) {
170
+ if (!classMatch && !methodMatch && !propMatch) {
171
+ pendingAttributes = [];
172
+ }
173
+ }
174
+ }
175
+
176
+ // Check for top-level statements (C# 9+)
177
+ const hasTopLevelStatements = !content.match(/^\s*(namespace|class|interface|struct|record)\s+\w+/m) &&
178
+ content.match(/^\s*(var|await|Console|app\.|builder\.)/m);
179
+
180
+ // Determine exports (public types)
181
+ const exports = classes
182
+ .filter(c => c.exported)
183
+ .map(c => ({
184
+ name: c.name,
185
+ type: c.type,
186
+ line: c.line
187
+ }));
188
+
189
+ return {
190
+ file: { path: filePath, relativePath },
191
+ content,
192
+ functions,
193
+ classes,
194
+ exports,
195
+ imports,
196
+ annotations,
197
+ lines: lines.length,
198
+ size: content.length,
199
+ parseMethod: 'csharp-regex',
200
+ metadata: {
201
+ namespace,
202
+ hasMainMethod: functions.some(f => f.isMainMethod),
203
+ hasTopLevelStatements,
204
+ isController: annotations.some(a =>
205
+ ['Controller', 'ApiController', 'ControllerBase'].includes(a.name)
206
+ ),
207
+ isAspNetCore: annotations.some(a =>
208
+ ['ApiController', 'Controller', 'HttpGet', 'HttpPost', 'Route', 'Authorize'].includes(a.name)
209
+ )
210
+ }
211
+ };
212
+
213
+ } catch (error) {
214
+ return createEmptyResult(filePath, relativePath, `Parse error: ${error.message}`);
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Parse method parameters
220
+ */
221
+ function parseParams(paramsStr) {
222
+ if (!paramsStr || !paramsStr.trim()) return [];
223
+
224
+ const params = [];
225
+ let depth = 0;
226
+ let current = '';
227
+
228
+ for (const char of paramsStr) {
229
+ if (char === '<' || char === '[') depth++;
230
+ else if (char === '>' || char === ']') depth--;
231
+ else if (char === ',' && depth === 0) {
232
+ if (current.trim()) {
233
+ params.push(parseParam(current.trim()));
234
+ }
235
+ current = '';
236
+ continue;
237
+ }
238
+ current += char;
239
+ }
240
+
241
+ if (current.trim()) {
242
+ params.push(parseParam(current.trim()));
243
+ }
244
+
245
+ return params;
246
+ }
247
+
248
+ /**
249
+ * Parse a single parameter
250
+ */
251
+ function parseParam(paramStr) {
252
+ // Remove attributes
253
+ const withoutAttrs = paramStr.replace(/\[\w+(?:\([^)]*\))?\]\s*/g, '');
254
+ // Handle modifiers like 'out', 'ref', 'in', 'params'
255
+ const parts = withoutAttrs.trim().split(/\s+/);
256
+
257
+ // Find the name (last part) and type (everything before, excluding modifiers)
258
+ const modifiers = ['out', 'ref', 'in', 'params', 'this'];
259
+ let nameIndex = parts.length - 1;
260
+
261
+ // Check for default value
262
+ const defaultIdx = parts.findIndex(p => p.includes('='));
263
+ if (defaultIdx !== -1) {
264
+ nameIndex = defaultIdx - 1;
265
+ }
266
+
267
+ const name = parts[nameIndex] || '';
268
+ const typeParts = parts.slice(0, nameIndex).filter(p => !modifiers.includes(p));
269
+
270
+ return {
271
+ type: typeParts.join(' '),
272
+ name: name.replace(/\s*=.*$/, ''),
273
+ modifiers: parts.filter(p => modifiers.includes(p))
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Find the end of a code block
279
+ */
280
+ function findBlockEnd(lines, startIndex) {
281
+ let braceCount = 0;
282
+ let started = false;
283
+
284
+ for (let i = startIndex; i < lines.length; i++) {
285
+ const line = lines[i];
286
+
287
+ for (let j = 0; j < line.length; j++) {
288
+ const char = line[j];
289
+
290
+ if (char === '{') {
291
+ braceCount++;
292
+ started = true;
293
+ } else if (char === '}') {
294
+ braceCount--;
295
+ if (started && braceCount === 0) {
296
+ return i + 1;
297
+ }
298
+ }
299
+ }
300
+ }
301
+
302
+ return startIndex + 1;
303
+ }
304
+
305
+ /**
306
+ * Find the end of a method (handles expression-bodied members)
307
+ */
308
+ function findMethodEnd(lines, startIndex) {
309
+ const line = lines[startIndex];
310
+
311
+ // Expression-bodied member: void Foo() => expr;
312
+ if (line.includes('=>') && line.includes(';')) {
313
+ return startIndex + 1;
314
+ }
315
+
316
+ // Abstract/interface method: void Foo();
317
+ if (line.trim().endsWith(';')) {
318
+ return startIndex + 1;
319
+ }
320
+
321
+ return findBlockEnd(lines, startIndex);
322
+ }
323
+
324
+ /**
325
+ * Extract code between line numbers
326
+ */
327
+ function extractCode(content, startLine, endLine) {
328
+ const lines = content.split('\n');
329
+ return lines.slice(startLine - 1, endLine).join('\n');
330
+ }
331
+
332
+ /**
333
+ * Create empty result
334
+ */
335
+ function createEmptyResult(filePath, relativePath, error) {
336
+ return {
337
+ file: { path: filePath, relativePath },
338
+ content: '',
339
+ functions: [],
340
+ classes: [],
341
+ exports: [],
342
+ imports: [],
343
+ annotations: [],
344
+ lines: 0,
345
+ size: 0,
346
+ error,
347
+ parseMethod: 'none'
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Check if a C# class has DI/framework attributes
353
+ */
354
+ export function hasDIAttributes(classInfo, diAttributes = []) {
355
+ const defaultAttributes = [
356
+ 'Controller', 'ApiController', 'ControllerBase',
357
+ 'Service', 'Scoped', 'Singleton', 'Transient',
358
+ 'Entity', 'Table', 'DbContext',
359
+ 'Injectable'
360
+ ];
361
+
362
+ const checkAttributes = new Set([...defaultAttributes, ...diAttributes]);
363
+ const classAttrs = (classInfo.attributes || classInfo.decorators || []).map(a => a.name);
364
+
365
+ const matched = classAttrs.filter(a => checkAttributes.has(a));
366
+
367
+ return {
368
+ hasDI: matched.length > 0,
369
+ attributes: matched
370
+ };
371
+ }
372
+
373
+ /**
374
+ * Check if a C# file is an entry point
375
+ */
376
+ export function isEntryPoint(parseResult) {
377
+ // Main method
378
+ if (parseResult.metadata?.hasMainMethod) {
379
+ return { isEntry: true, reason: 'Has Main() method' };
380
+ }
381
+
382
+ // Top-level statements (C# 9+)
383
+ if (parseResult.metadata?.hasTopLevelStatements) {
384
+ return { isEntry: true, reason: 'Has top-level statements' };
385
+ }
386
+
387
+ // Program.cs or Startup.cs
388
+ const fileName = parseResult.file?.relativePath || '';
389
+ if (fileName.endsWith('Program.cs') || fileName.endsWith('Startup.cs')) {
390
+ return { isEntry: true, reason: 'Is Program.cs or Startup.cs' };
391
+ }
392
+
393
+ return { isEntry: false };
394
+ }
395
+
396
+ export default {
397
+ parse,
398
+ hasDIAttributes,
399
+ isEntryPoint
400
+ };
@@ -0,0 +1,60 @@
1
+ // src/scanner/parsers/css.mjs
2
+ // CSS parser
3
+
4
+ import { readFileSync, existsSync } from 'fs';
5
+
6
+ /**
7
+ * Parse a CSS file
8
+ */
9
+ export async function parseCSS(file) {
10
+ const filePath = typeof file === 'string' ? file : file.path;
11
+ const relativePath = typeof file === 'string' ? file : file.relativePath;
12
+
13
+ if (!existsSync(filePath)) {
14
+ return {
15
+ file: { path: filePath, relativePath },
16
+ content: '',
17
+ selectors: [],
18
+ rules: 0,
19
+ lines: 0
20
+ };
21
+ }
22
+
23
+ try {
24
+ const content = readFileSync(filePath, 'utf-8');
25
+ const lines = content.split('\n');
26
+
27
+ // Basic parsing - count selectors and rules
28
+ const selectorMatches = content.match(/[^{}]+(?=\{)/g) || [];
29
+ const selectors = selectorMatches.map(s => s.trim()).filter(s => s.length > 0);
30
+
31
+ // Extract CSS package references: @import, @plugin, @use
32
+ const imports = [];
33
+ const cssImportRe = /(?:@import\s+(?:url\(\s*)?|@plugin\s+|@use\s+)['"]([^'"]+)['"]/g;
34
+ let m;
35
+ while ((m = cssImportRe.exec(content)) !== null) {
36
+ imports.push({ module: m[1], type: 'css-directive' });
37
+ }
38
+
39
+ return {
40
+ file: { path: filePath, relativePath },
41
+ content,
42
+ selectors,
43
+ rules: selectors.length,
44
+ lines: lines.length,
45
+ size: content.length,
46
+ imports
47
+ };
48
+ } catch (error) {
49
+ return {
50
+ file: { path: filePath, relativePath },
51
+ content: '',
52
+ selectors: [],
53
+ rules: 0,
54
+ lines: 0,
55
+ error: error.message
56
+ };
57
+ }
58
+ }
59
+
60
+ export default { parseCSS };