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,497 @@
|
|
|
1
|
+
// src/scanner/parsers/python.mjs
|
|
2
|
+
// Python parser with Django, FastAPI, Flask, Celery support
|
|
3
|
+
|
|
4
|
+
import { readFileSync, existsSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse a Python file and extract classes, functions, decorators, 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 decorators = []; // All decorators found
|
|
31
|
+
const imports = [];
|
|
32
|
+
|
|
33
|
+
// Track decorators for next element
|
|
34
|
+
let pendingDecorators = [];
|
|
35
|
+
|
|
36
|
+
// Parse line by line
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
const line = lines[i];
|
|
39
|
+
const lineNum = i + 1;
|
|
40
|
+
|
|
41
|
+
// Detect import statements
|
|
42
|
+
const importMatch = line.match(/^\s*import\s+([\w.]+)(?:\s+as\s+(\w+))?/);
|
|
43
|
+
if (importMatch) {
|
|
44
|
+
imports.push({
|
|
45
|
+
module: importMatch[1],
|
|
46
|
+
alias: importMatch[2] || null,
|
|
47
|
+
type: 'import',
|
|
48
|
+
line: lineNum
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Detect from ... import statements (single-line and multi-line with parentheses)
|
|
54
|
+
const fromImportMatch = line.match(/^\s*from\s+([\w.]+)\s+import\s+(.+)/);
|
|
55
|
+
if (fromImportMatch) {
|
|
56
|
+
const module = fromImportMatch[1];
|
|
57
|
+
let importedText = fromImportMatch[2].trim();
|
|
58
|
+
|
|
59
|
+
// Handle multi-line parenthetical imports: from X import (\n item1,\n item2\n)
|
|
60
|
+
if (importedText.startsWith('(') && !importedText.includes(')')) {
|
|
61
|
+
// Collect lines until closing paren
|
|
62
|
+
importedText = importedText.slice(1); // remove opening paren
|
|
63
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
64
|
+
const nextLine = lines[j].trim();
|
|
65
|
+
if (nextLine.includes(')')) {
|
|
66
|
+
importedText += ',' + nextLine.replace(')', '');
|
|
67
|
+
i = j; // advance line pointer
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
importedText += ',' + nextLine;
|
|
71
|
+
}
|
|
72
|
+
} else if (importedText.startsWith('(') && importedText.includes(')')) {
|
|
73
|
+
// Single-line parenthetical: from X import (a, b, c)
|
|
74
|
+
importedText = importedText.replace(/[()]/g, '');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const importedItems = importedText.split(',').map(s => s.trim()).filter(s => s && !s.startsWith('#'));
|
|
78
|
+
|
|
79
|
+
// If module is only dots (e.g. ".", ".."), imported items are submodules
|
|
80
|
+
// e.g. "from . import applications" → module should be ".applications"
|
|
81
|
+
// If module has a name after dots (e.g. ".applications"), imported items are symbols
|
|
82
|
+
// e.g. "from .applications import FastAPI" → module should be ".applications"
|
|
83
|
+
const isDotsOnly = /^\.+$/.test(module);
|
|
84
|
+
|
|
85
|
+
for (const item of importedItems) {
|
|
86
|
+
// Handle star import: from X import *
|
|
87
|
+
if (item.trim() === '*') {
|
|
88
|
+
imports.push({
|
|
89
|
+
module: module,
|
|
90
|
+
name: '*',
|
|
91
|
+
alias: null,
|
|
92
|
+
type: 'from',
|
|
93
|
+
line: lineNum
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const aliasMatch = item.match(/(\w+)(?:\s+as\s+(\w+))?/);
|
|
98
|
+
if (aliasMatch) {
|
|
99
|
+
imports.push({
|
|
100
|
+
module: isDotsOnly ? `${module}${aliasMatch[1]}` : module,
|
|
101
|
+
name: aliasMatch[1],
|
|
102
|
+
alias: aliasMatch[2] || null,
|
|
103
|
+
type: 'from',
|
|
104
|
+
line: lineNum
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Detect decorators
|
|
112
|
+
const decoratorMatch = line.match(/^\s*@([\w.]+)(?:\(([^)]*)\))?/);
|
|
113
|
+
if (decoratorMatch) {
|
|
114
|
+
const decorator = {
|
|
115
|
+
name: decoratorMatch[1],
|
|
116
|
+
args: decoratorMatch[2] || null,
|
|
117
|
+
line: lineNum
|
|
118
|
+
};
|
|
119
|
+
decorators.push(decorator);
|
|
120
|
+
pendingDecorators.push(decorator);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Detect class declaration
|
|
125
|
+
const classMatch = line.match(/^(\s*)class\s+(\w+)(?:\(([^)]*)\))?:/);
|
|
126
|
+
if (classMatch) {
|
|
127
|
+
const indent = classMatch[1].length;
|
|
128
|
+
const baseClasses = classMatch[3] ? classMatch[3].split(',').map(s => s.trim()) : [];
|
|
129
|
+
|
|
130
|
+
const classInfo = {
|
|
131
|
+
name: classMatch[2],
|
|
132
|
+
type: 'class',
|
|
133
|
+
line: lineNum,
|
|
134
|
+
endLine: findIndentBlockEnd(lines, i, indent),
|
|
135
|
+
indent,
|
|
136
|
+
baseClasses,
|
|
137
|
+
decorators: [...pendingDecorators],
|
|
138
|
+
methods: [],
|
|
139
|
+
exported: !classMatch[2].startsWith('_') // Not private
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
classInfo.lineCount = classInfo.endLine - classInfo.line + 1;
|
|
143
|
+
classInfo.sizeBytes = extractCode(content, classInfo.line, classInfo.endLine).length;
|
|
144
|
+
|
|
145
|
+
// Parse methods inside the class
|
|
146
|
+
parseClassMethods(lines, classInfo, functions);
|
|
147
|
+
|
|
148
|
+
classes.push(classInfo);
|
|
149
|
+
pendingDecorators = [];
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Detect function declaration (module-level)
|
|
154
|
+
const funcMatch = line.match(/^(\s*)(async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([\w\[\],\s.]+))?:/);
|
|
155
|
+
if (funcMatch && funcMatch[1].length === 0) { // Module-level only
|
|
156
|
+
const funcInfo = {
|
|
157
|
+
name: funcMatch[3],
|
|
158
|
+
type: funcMatch[2] ? 'async function' : 'function',
|
|
159
|
+
async: !!funcMatch[2],
|
|
160
|
+
line: lineNum,
|
|
161
|
+
endLine: findIndentBlockEnd(lines, i, 0),
|
|
162
|
+
params: parseParams(funcMatch[4]),
|
|
163
|
+
returnType: funcMatch[5]?.trim() || null,
|
|
164
|
+
decorators: [...pendingDecorators],
|
|
165
|
+
signature: `def ${funcMatch[3]}(${funcMatch[4]})`,
|
|
166
|
+
exported: !funcMatch[3].startsWith('_')
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
funcInfo.lineCount = funcInfo.endLine - funcInfo.line + 1;
|
|
170
|
+
funcInfo.sizeBytes = extractCode(content, funcInfo.line, funcInfo.endLine).length;
|
|
171
|
+
|
|
172
|
+
functions.push(funcInfo);
|
|
173
|
+
pendingDecorators = [];
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Clear pending decorators if we hit something else
|
|
178
|
+
if (line.trim() && !line.trim().startsWith('#')) {
|
|
179
|
+
pendingDecorators = [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check for __main__ block
|
|
184
|
+
const hasMainBlock = content.includes('if __name__ == "__main__"') ||
|
|
185
|
+
content.includes("if __name__ == '__main__'");
|
|
186
|
+
|
|
187
|
+
// Determine exports (public functions and classes)
|
|
188
|
+
const exports = [
|
|
189
|
+
...functions.filter(f => f.exported).map(f => ({
|
|
190
|
+
name: f.name,
|
|
191
|
+
type: 'function',
|
|
192
|
+
line: f.line
|
|
193
|
+
})),
|
|
194
|
+
...classes.filter(c => c.exported).map(c => ({
|
|
195
|
+
name: c.name,
|
|
196
|
+
type: 'class',
|
|
197
|
+
line: c.line
|
|
198
|
+
}))
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
// Check for __all__ definition
|
|
202
|
+
const allMatch = content.match(/__all__\s*=\s*\[([\s\S]*?)\]/);
|
|
203
|
+
if (allMatch) {
|
|
204
|
+
const exportNames = allMatch[1].match(/['"](\w+)['"]/g);
|
|
205
|
+
if (exportNames) {
|
|
206
|
+
// __all__ defines explicit exports
|
|
207
|
+
exports.length = 0;
|
|
208
|
+
for (const name of exportNames) {
|
|
209
|
+
const cleanName = name.replace(/['"]/g, '');
|
|
210
|
+
exports.push({
|
|
211
|
+
name: cleanName,
|
|
212
|
+
type: 'explicit',
|
|
213
|
+
line: 0
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
file: { path: filePath, relativePath },
|
|
221
|
+
content,
|
|
222
|
+
functions,
|
|
223
|
+
classes,
|
|
224
|
+
exports,
|
|
225
|
+
imports,
|
|
226
|
+
annotations: decorators,
|
|
227
|
+
lines: lines.length,
|
|
228
|
+
size: content.length,
|
|
229
|
+
parseMethod: 'python-regex',
|
|
230
|
+
metadata: {
|
|
231
|
+
hasMainBlock,
|
|
232
|
+
isDjangoModel: classes.some(c =>
|
|
233
|
+
c.baseClasses.some(b => b.includes('Model') || b.includes('models.Model'))
|
|
234
|
+
),
|
|
235
|
+
isDjangoView: classes.some(c =>
|
|
236
|
+
c.baseClasses.some(b => b.includes('View') || b.includes('APIView') || b.includes('ViewSet'))
|
|
237
|
+
),
|
|
238
|
+
isFastAPI: decorators.some(d =>
|
|
239
|
+
d.name.includes('app.') || d.name.includes('router.') || d.name === 'Depends'
|
|
240
|
+
),
|
|
241
|
+
isFlask: decorators.some(d =>
|
|
242
|
+
d.name.includes('route') || d.name.includes('Blueprint')
|
|
243
|
+
),
|
|
244
|
+
isCelery: decorators.some(d =>
|
|
245
|
+
d.name === 'task' || d.name === 'shared_task' || d.name.includes('celery.')
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
} catch (error) {
|
|
251
|
+
return createEmptyResult(filePath, relativePath, `Parse error: ${error.message}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Parse methods inside a class
|
|
257
|
+
*/
|
|
258
|
+
function parseClassMethods(lines, classInfo, allFunctions) {
|
|
259
|
+
const classIndent = classInfo.indent;
|
|
260
|
+
|
|
261
|
+
for (let i = classInfo.line; i < classInfo.endLine - 1 && i < lines.length; i++) {
|
|
262
|
+
const line = lines[i];
|
|
263
|
+
const lineNum = i + 1;
|
|
264
|
+
|
|
265
|
+
// Method declaration
|
|
266
|
+
const methodMatch = line.match(/^(\s+)(async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([\w\[\],\s.]+))?:/);
|
|
267
|
+
if (methodMatch) {
|
|
268
|
+
const methodIndent = methodMatch[1].length;
|
|
269
|
+
// Must be directly inside the class
|
|
270
|
+
if (methodIndent > classIndent) {
|
|
271
|
+
// Collect decorators from previous lines
|
|
272
|
+
const methodDecorators = [];
|
|
273
|
+
for (let j = i - 1; j >= classInfo.line; j--) {
|
|
274
|
+
const prevLine = lines[j];
|
|
275
|
+
const decMatch = prevLine.match(/^\s+@([\w.]+)(?:\(([^)]*)\))?/);
|
|
276
|
+
if (decMatch) {
|
|
277
|
+
methodDecorators.unshift({
|
|
278
|
+
name: decMatch[1],
|
|
279
|
+
args: decMatch[2] || null,
|
|
280
|
+
line: j + 1
|
|
281
|
+
});
|
|
282
|
+
} else if (prevLine.trim() && !prevLine.trim().startsWith('#')) {
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const methodInfo = {
|
|
288
|
+
name: methodMatch[3],
|
|
289
|
+
type: methodMatch[2] ? 'async method' : 'method',
|
|
290
|
+
async: !!methodMatch[2],
|
|
291
|
+
className: classInfo.name,
|
|
292
|
+
line: lineNum,
|
|
293
|
+
endLine: findIndentBlockEnd(lines, i, methodIndent),
|
|
294
|
+
params: parseParams(methodMatch[4]),
|
|
295
|
+
returnType: methodMatch[5]?.trim() || null,
|
|
296
|
+
decorators: methodDecorators,
|
|
297
|
+
signature: `def ${methodMatch[3]}(${methodMatch[4]})`,
|
|
298
|
+
isStatic: methodDecorators.some(d => d.name === 'staticmethod'),
|
|
299
|
+
isClassMethod: methodDecorators.some(d => d.name === 'classmethod'),
|
|
300
|
+
isProperty: methodDecorators.some(d => d.name === 'property')
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
methodInfo.lineCount = methodInfo.endLine - methodInfo.line + 1;
|
|
304
|
+
|
|
305
|
+
classInfo.methods.push(methodInfo);
|
|
306
|
+
allFunctions.push(methodInfo);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Parse function parameters
|
|
314
|
+
*/
|
|
315
|
+
function parseParams(paramsStr) {
|
|
316
|
+
if (!paramsStr || !paramsStr.trim()) return [];
|
|
317
|
+
|
|
318
|
+
const params = [];
|
|
319
|
+
let depth = 0;
|
|
320
|
+
let current = '';
|
|
321
|
+
|
|
322
|
+
for (const char of paramsStr) {
|
|
323
|
+
if (char === '[' || char === '(') depth++;
|
|
324
|
+
else if (char === ']' || char === ')') depth--;
|
|
325
|
+
else if (char === ',' && depth === 0) {
|
|
326
|
+
if (current.trim()) {
|
|
327
|
+
params.push(parseParam(current.trim()));
|
|
328
|
+
}
|
|
329
|
+
current = '';
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
current += char;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (current.trim()) {
|
|
336
|
+
params.push(parseParam(current.trim()));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return params;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Parse a single parameter
|
|
344
|
+
*/
|
|
345
|
+
function parseParam(paramStr) {
|
|
346
|
+
// Handle: name, name: type, name = default, name: type = default, *args, **kwargs
|
|
347
|
+
const match = paramStr.match(/^(\*{0,2})(\w+)(?:\s*:\s*([^=]+))?(?:\s*=\s*(.+))?$/);
|
|
348
|
+
if (match) {
|
|
349
|
+
return {
|
|
350
|
+
name: match[2],
|
|
351
|
+
type: match[3]?.trim() || null,
|
|
352
|
+
default: match[4]?.trim() || null,
|
|
353
|
+
isVararg: match[1] === '*',
|
|
354
|
+
isKwarg: match[1] === '**'
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return { name: paramStr, type: null };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Find end of indented block
|
|
362
|
+
*/
|
|
363
|
+
function findIndentBlockEnd(lines, startIndex, baseIndent) {
|
|
364
|
+
const firstLine = lines[startIndex];
|
|
365
|
+
const firstIndent = firstLine.match(/^(\s*)/)[1].length;
|
|
366
|
+
|
|
367
|
+
// If we're looking for module-level block end, use the function's indent
|
|
368
|
+
const targetIndent = baseIndent === 0 ? firstIndent : baseIndent;
|
|
369
|
+
|
|
370
|
+
for (let i = startIndex + 1; i < lines.length; i++) {
|
|
371
|
+
const line = lines[i];
|
|
372
|
+
|
|
373
|
+
// Skip empty lines and comments
|
|
374
|
+
if (!line.trim() || line.trim().startsWith('#')) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Check indent
|
|
379
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
380
|
+
|
|
381
|
+
// If we find a line with less or equal indent (for non-module level)
|
|
382
|
+
// or less indent (for module level), block ends
|
|
383
|
+
if (baseIndent === 0) {
|
|
384
|
+
// Module-level function: ends when we see another module-level statement
|
|
385
|
+
if (indent === 0 && line.trim()) {
|
|
386
|
+
return i;
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
// Class/nested: ends when we see something at class level or less
|
|
390
|
+
if (indent <= targetIndent && line.trim()) {
|
|
391
|
+
return i;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return lines.length;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Extract code between line numbers
|
|
401
|
+
*/
|
|
402
|
+
function extractCode(content, startLine, endLine) {
|
|
403
|
+
const lines = content.split('\n');
|
|
404
|
+
return lines.slice(startLine - 1, endLine).join('\n');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Create empty result
|
|
409
|
+
*/
|
|
410
|
+
function createEmptyResult(filePath, relativePath, error) {
|
|
411
|
+
return {
|
|
412
|
+
file: { path: filePath, relativePath },
|
|
413
|
+
content: '',
|
|
414
|
+
functions: [],
|
|
415
|
+
classes: [],
|
|
416
|
+
exports: [],
|
|
417
|
+
imports: [],
|
|
418
|
+
annotations: [],
|
|
419
|
+
lines: 0,
|
|
420
|
+
size: 0,
|
|
421
|
+
error,
|
|
422
|
+
parseMethod: 'none'
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Check if a Python class is a framework component
|
|
428
|
+
*/
|
|
429
|
+
export function isFrameworkComponent(classInfo, parseResult) {
|
|
430
|
+
// Django Model
|
|
431
|
+
if (classInfo.baseClasses.some(b => b.includes('Model') || b.includes('models.Model'))) {
|
|
432
|
+
return { is: true, framework: 'django', type: 'model' };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Django View
|
|
436
|
+
if (classInfo.baseClasses.some(b =>
|
|
437
|
+
b.includes('View') || b.includes('APIView') || b.includes('ViewSet') ||
|
|
438
|
+
b.includes('GenericAPIView') || b.includes('ModelViewSet')
|
|
439
|
+
)) {
|
|
440
|
+
return { is: true, framework: 'django', type: 'view' };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Django Admin
|
|
444
|
+
if (classInfo.baseClasses.some(b => b.includes('ModelAdmin') || b.includes('admin.ModelAdmin'))) {
|
|
445
|
+
return { is: true, framework: 'django', type: 'admin' };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Django Form
|
|
449
|
+
if (classInfo.baseClasses.some(b =>
|
|
450
|
+
b.includes('Form') || b.includes('ModelForm') || b.includes('forms.Form')
|
|
451
|
+
)) {
|
|
452
|
+
return { is: true, framework: 'django', type: 'form' };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// FastAPI/Flask router decorators on functions
|
|
456
|
+
for (const decorator of classInfo.decorators || []) {
|
|
457
|
+
if (decorator.name.includes('router.') || decorator.name.includes('app.')) {
|
|
458
|
+
return { is: true, framework: 'fastapi', type: 'route' };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return { is: false };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Check if a Python file is an entry point
|
|
467
|
+
*/
|
|
468
|
+
export function isEntryPoint(parseResult) {
|
|
469
|
+
// Has if __name__ == "__main__"
|
|
470
|
+
if (parseResult.metadata?.hasMainBlock) {
|
|
471
|
+
return { isEntry: true, reason: 'Has __main__ block' };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Django manage.py style
|
|
475
|
+
const fileName = parseResult.file?.relativePath || '';
|
|
476
|
+
if (fileName.endsWith('manage.py')) {
|
|
477
|
+
return { isEntry: true, reason: 'Is Django manage.py' };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Django wsgi.py/asgi.py
|
|
481
|
+
if (fileName.endsWith('wsgi.py') || fileName.endsWith('asgi.py')) {
|
|
482
|
+
return { isEntry: true, reason: 'Is WSGI/ASGI entry point' };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Celery tasks are entry points (executed by worker)
|
|
486
|
+
if (parseResult.metadata?.isCelery) {
|
|
487
|
+
return { isEntry: true, reason: 'Has Celery task decorators' };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return { isEntry: false };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export default {
|
|
494
|
+
parse,
|
|
495
|
+
isFrameworkComponent,
|
|
496
|
+
isEntryPoint
|
|
497
|
+
};
|