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