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