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