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,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
|
+
};
|