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,350 @@
1
+ // src/scanner/parsers/kotlin.mjs
2
+ // Kotlin parser with Spring Boot annotation support
3
+
4
+ import { readFileSync, existsSync } from 'fs';
5
+
6
+ /**
7
+ * Parse a Kotlin file and extract classes, functions, annotations, 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 annotations = [];
31
+ const imports = [];
32
+ let packageName = null;
33
+
34
+ // Extract package declaration
35
+ const packageMatch = content.match(/^\s*package\s+([\w.]+)/m);
36
+ if (packageMatch) {
37
+ packageName = packageMatch[1];
38
+ }
39
+
40
+ // Extract imports
41
+ const importPattern = /^\s*import\s+([\w.*]+)(?:\s+as\s+(\w+))?/gm;
42
+ let importMatch;
43
+ while ((importMatch = importPattern.exec(content)) !== null) {
44
+ const lineNum = content.substring(0, importMatch.index).split('\n').length;
45
+ imports.push({
46
+ module: importMatch[1],
47
+ alias: importMatch[2] || null,
48
+ type: 'normal',
49
+ line: lineNum
50
+ });
51
+ }
52
+
53
+ // Track annotations on current element
54
+ let pendingAnnotations = [];
55
+
56
+ // Parse line by line
57
+ for (let i = 0; i < lines.length; i++) {
58
+ const line = lines[i];
59
+ const lineNum = i + 1;
60
+
61
+ // Detect annotations
62
+ const annotationPattern = /@(\w+)(?:\s*\(([^)]*)\))?/g;
63
+ let annotationMatch;
64
+ while ((annotationMatch = annotationPattern.exec(line)) !== null) {
65
+ const annotation = {
66
+ name: annotationMatch[1],
67
+ args: annotationMatch[2] || null,
68
+ line: lineNum
69
+ };
70
+ annotations.push(annotation);
71
+ pendingAnnotations.push(annotation);
72
+ }
73
+
74
+ // Detect class/interface/object declaration
75
+ const classMatch = line.match(/^\s*(public|private|protected|internal)?\s*(open|abstract|sealed|final|data|enum|annotation)?\s*(class|interface|object)\s+(\w+)(?:<[^>]+>)?(?:\s*:\s*([^{]+))?/);
76
+ if (classMatch) {
77
+ const classInfo = {
78
+ name: classMatch[4],
79
+ type: classMatch[3],
80
+ visibility: classMatch[1] || 'public',
81
+ modifiers: classMatch[2] ? [classMatch[2]] : [],
82
+ line: lineNum,
83
+ endLine: findBlockEnd(lines, i),
84
+ superTypes: classMatch[5] ? classMatch[5].split(',').map(s => s.trim().split('(')[0].trim()) : [],
85
+ decorators: [...pendingAnnotations],
86
+ annotations: [...pendingAnnotations],
87
+ methods: [],
88
+ exported: classMatch[1] !== 'private' && classMatch[1] !== 'internal'
89
+ };
90
+
91
+ classInfo.lineCount = classInfo.endLine - classInfo.line + 1;
92
+ classInfo.sizeBytes = extractCode(content, classInfo.line, classInfo.endLine).length;
93
+
94
+ classes.push(classInfo);
95
+ pendingAnnotations = [];
96
+ }
97
+
98
+ // Detect function declaration
99
+ const funcMatch = line.match(/^\s*(public|private|protected|internal)?\s*(open|override|final|suspend)?\s*fun\s+(?:<[^>]+>\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*([\w<>,\s?*]+))?/);
100
+ if (funcMatch) {
101
+ const funcInfo = {
102
+ name: funcMatch[3],
103
+ type: funcMatch[2] === 'suspend' ? 'suspend function' : 'function',
104
+ visibility: funcMatch[1] || 'public',
105
+ modifiers: funcMatch[2] ? [funcMatch[2]] : [],
106
+ line: lineNum,
107
+ endLine: findBlockEnd(lines, i),
108
+ params: parseParams(funcMatch[4]),
109
+ returnType: funcMatch[5]?.trim() || null,
110
+ decorators: [...pendingAnnotations],
111
+ annotations: [...pendingAnnotations],
112
+ signature: `fun ${funcMatch[3]}(${funcMatch[4]})`
113
+ };
114
+
115
+ funcInfo.lineCount = funcInfo.endLine - funcInfo.line + 1;
116
+ funcInfo.sizeBytes = extractCode(content, funcInfo.line, funcInfo.endLine).length;
117
+
118
+ // Check for main function
119
+ if (funcMatch[3] === 'main') {
120
+ funcInfo.isMainFunction = true;
121
+ }
122
+
123
+ functions.push(funcInfo);
124
+
125
+ // Add to current class if we're inside one
126
+ if (classes.length > 0) {
127
+ const currentClass = classes[classes.length - 1];
128
+ if (lineNum > currentClass.line && lineNum < currentClass.endLine) {
129
+ currentClass.methods.push(funcInfo);
130
+ }
131
+ }
132
+
133
+ pendingAnnotations = [];
134
+ }
135
+
136
+ // Clear pending annotations if we hit a non-annotation line
137
+ if (line.trim() && !line.trim().startsWith('@') && !line.trim().startsWith('//') && !line.trim().startsWith('/*')) {
138
+ if (!classMatch && !funcMatch) {
139
+ pendingAnnotations = [];
140
+ }
141
+ }
142
+ }
143
+
144
+ // Determine exports (public classes and functions)
145
+ const exports = [
146
+ ...classes.filter(c => c.exported).map(c => ({
147
+ name: c.name,
148
+ type: c.type,
149
+ line: c.line
150
+ })),
151
+ ...functions.filter(f => f.visibility !== 'private' && f.visibility !== 'internal').map(f => ({
152
+ name: f.name,
153
+ type: 'function',
154
+ line: f.line
155
+ }))
156
+ ];
157
+
158
+ return {
159
+ file: { path: filePath, relativePath },
160
+ content,
161
+ functions,
162
+ classes,
163
+ exports,
164
+ imports,
165
+ annotations,
166
+ lines: lines.length,
167
+ size: content.length,
168
+ parseMethod: 'kotlin-regex',
169
+ metadata: {
170
+ packageName,
171
+ hasMainFunction: functions.some(f => f.isMainFunction),
172
+ isSpringComponent: annotations.some(a =>
173
+ ['Component', 'Service', 'Repository', 'Controller', 'RestController', 'Configuration', 'SpringBootApplication',
174
+ 'ApplicationScoped', 'RequestScoped', 'SessionScoped', 'Dependent', 'Singleton', 'Named',
175
+ 'Stateless', 'Stateful', 'MessageDriven', 'Path', 'Provider',
176
+ 'QuarkusMain', 'Entity', 'MappedSuperclass', 'Converter'].includes(a.name)
177
+ )
178
+ }
179
+ };
180
+
181
+ } catch (error) {
182
+ return createEmptyResult(filePath, relativePath, `Parse error: ${error.message}`);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Parse function parameters
188
+ */
189
+ function parseParams(paramsStr) {
190
+ if (!paramsStr || !paramsStr.trim()) return [];
191
+
192
+ const params = [];
193
+ let depth = 0;
194
+ let current = '';
195
+
196
+ for (const char of paramsStr) {
197
+ if (char === '<' || char === '(') depth++;
198
+ else if (char === '>' || char === ')') depth--;
199
+ else if (char === ',' && depth === 0) {
200
+ if (current.trim()) {
201
+ params.push(parseParam(current.trim()));
202
+ }
203
+ current = '';
204
+ continue;
205
+ }
206
+ current += char;
207
+ }
208
+
209
+ if (current.trim()) {
210
+ params.push(parseParam(current.trim()));
211
+ }
212
+
213
+ return params;
214
+ }
215
+
216
+ /**
217
+ * Parse a single parameter
218
+ */
219
+ function parseParam(paramStr) {
220
+ // Handle: name: Type, name: Type = default, vararg name: Type
221
+ const match = paramStr.match(/^(vararg\s+)?(\w+)\s*:\s*(.+?)(?:\s*=\s*(.+))?$/);
222
+ if (match) {
223
+ return {
224
+ name: match[2],
225
+ type: match[3].trim(),
226
+ default: match[4]?.trim() || null,
227
+ isVararg: !!match[1]
228
+ };
229
+ }
230
+ return { name: paramStr, type: null };
231
+ }
232
+
233
+ /**
234
+ * Find end of a code block (matching braces)
235
+ */
236
+ function findBlockEnd(lines, startIndex) {
237
+ let braceCount = 0;
238
+ let started = false;
239
+
240
+ for (let i = startIndex; i < lines.length; i++) {
241
+ const line = lines[i];
242
+
243
+ let inString = false;
244
+ let stringChar = '';
245
+
246
+ for (let j = 0; j < line.length; j++) {
247
+ const char = line[j];
248
+ const nextChar = line[j + 1];
249
+
250
+ if (!inString && (char === '"' || char === "'")) {
251
+ inString = true;
252
+ stringChar = char;
253
+ continue;
254
+ }
255
+ if (inString && char === stringChar && line[j - 1] !== '\\') {
256
+ inString = false;
257
+ continue;
258
+ }
259
+ if (inString) continue;
260
+
261
+ if (char === '/' && nextChar === '/') break;
262
+
263
+ if (char === '{') {
264
+ braceCount++;
265
+ started = true;
266
+ } else if (char === '}') {
267
+ braceCount--;
268
+ if (started && braceCount === 0) {
269
+ return i + 1;
270
+ }
271
+ }
272
+ }
273
+ }
274
+
275
+ return startIndex + 1;
276
+ }
277
+
278
+ /**
279
+ * Extract code between line numbers
280
+ */
281
+ function extractCode(content, startLine, endLine) {
282
+ const lines = content.split('\n');
283
+ return lines.slice(startLine - 1, endLine).join('\n');
284
+ }
285
+
286
+ /**
287
+ * Create empty result
288
+ */
289
+ function createEmptyResult(filePath, relativePath, error) {
290
+ return {
291
+ file: { path: filePath, relativePath },
292
+ content: '',
293
+ functions: [],
294
+ classes: [],
295
+ exports: [],
296
+ imports: [],
297
+ annotations: [],
298
+ lines: 0,
299
+ size: 0,
300
+ error,
301
+ parseMethod: 'none'
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Check if a Kotlin class has Spring/DI annotations
307
+ */
308
+ export function hasDIAnnotations(classInfo, diAnnotations = []) {
309
+ const defaultAnnotations = [
310
+ 'Component', 'Service', 'Repository', 'Controller', 'RestController',
311
+ 'Configuration', 'Bean', 'SpringBootApplication',
312
+ 'Inject', 'Singleton', 'Module', 'Provides'
313
+ ];
314
+
315
+ const checkAnnotations = new Set([...defaultAnnotations, ...diAnnotations]);
316
+ const classAnnotations = (classInfo.annotations || classInfo.decorators || []).map(a => a.name);
317
+
318
+ const matched = classAnnotations.filter(a => checkAnnotations.has(a));
319
+
320
+ return {
321
+ hasDI: matched.length > 0,
322
+ annotations: matched
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Check if a Kotlin file is an entry point
328
+ */
329
+ export function isEntryPoint(parseResult) {
330
+ // Check for main function
331
+ if (parseResult.metadata?.hasMainFunction) {
332
+ return { isEntry: true, reason: 'Has fun main()' };
333
+ }
334
+
335
+ // Check for @SpringBootApplication
336
+ if (parseResult.metadata?.isSpringComponent) {
337
+ const springApp = parseResult.annotations.find(a => a.name === 'SpringBootApplication');
338
+ if (springApp) {
339
+ return { isEntry: true, reason: 'Has @SpringBootApplication annotation' };
340
+ }
341
+ }
342
+
343
+ return { isEntry: false };
344
+ }
345
+
346
+ export default {
347
+ parse,
348
+ hasDIAnnotations,
349
+ isEntryPoint
350
+ };