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,233 @@
|
|
|
1
|
+
// src/scanner/parsers/registry.mjs
|
|
2
|
+
// Multi-language parser registry with lazy loading
|
|
3
|
+
|
|
4
|
+
import { extname } from 'path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parser result structure (common across all languages)
|
|
8
|
+
* @typedef {Object} ParseResult
|
|
9
|
+
* @property {Object} file - File info (path, relativePath)
|
|
10
|
+
* @property {string} content - File content
|
|
11
|
+
* @property {Array} functions - Detected functions/methods
|
|
12
|
+
* @property {Array} classes - Detected classes/types
|
|
13
|
+
* @property {Array} exports - Detected exports
|
|
14
|
+
* @property {Array} imports - Detected imports/dependencies
|
|
15
|
+
* @property {Array} annotations - Detected annotations/decorators
|
|
16
|
+
* @property {number} lines - Line count
|
|
17
|
+
* @property {number} size - Byte size
|
|
18
|
+
* @property {string} parseMethod - Parser used
|
|
19
|
+
* @property {string} [error] - Error message if parsing failed
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parser registry with lazy loading
|
|
24
|
+
* Each parser is loaded on-demand to reduce startup time
|
|
25
|
+
*/
|
|
26
|
+
const parserRegistry = {
|
|
27
|
+
// JavaScript/TypeScript (primary, always loaded)
|
|
28
|
+
'.js': () => import('./javascript.mjs'),
|
|
29
|
+
'.mjs': () => import('./javascript.mjs'),
|
|
30
|
+
'.cjs': () => import('./javascript.mjs'),
|
|
31
|
+
'.jsx': () => import('./javascript.mjs'),
|
|
32
|
+
'.ts': () => import('./javascript.mjs'),
|
|
33
|
+
'.mts': () => import('./javascript.mjs'),
|
|
34
|
+
'.cts': () => import('./javascript.mjs'),
|
|
35
|
+
'.tsx': () => import('./javascript.mjs'),
|
|
36
|
+
'.vue': () => import('./javascript.mjs'),
|
|
37
|
+
'.svelte': () => import('./javascript.mjs'),
|
|
38
|
+
|
|
39
|
+
// Java/Kotlin (JVM)
|
|
40
|
+
'.java': () => import('./java.mjs'),
|
|
41
|
+
'.kt': () => import('./kotlin.mjs'),
|
|
42
|
+
'.kts': () => import('./kotlin.mjs'),
|
|
43
|
+
|
|
44
|
+
// .NET
|
|
45
|
+
'.cs': () => import('./csharp.mjs'),
|
|
46
|
+
// '.fs': () => import('./fsharp.mjs'), // TODO: Not implemented
|
|
47
|
+
// '.vb': () => import('./vb.mjs'), // TODO: Not implemented
|
|
48
|
+
|
|
49
|
+
// Python
|
|
50
|
+
'.py': () => import('./python.mjs'),
|
|
51
|
+
'.pyi': () => import('./python.mjs'),
|
|
52
|
+
|
|
53
|
+
// Go
|
|
54
|
+
'.go': () => import('./go.mjs'),
|
|
55
|
+
|
|
56
|
+
// Rust
|
|
57
|
+
'.rs': () => import('./rust.mjs')
|
|
58
|
+
|
|
59
|
+
// TODO: Future language support
|
|
60
|
+
// '.rb': () => import('./ruby.mjs'),
|
|
61
|
+
// '.php': () => import('./php.mjs'),
|
|
62
|
+
// '.swift': () => import('./swift.mjs'),
|
|
63
|
+
// '.scala': () => import('./scala.mjs'),
|
|
64
|
+
// '.sc': () => import('./scala.mjs')
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Cache for loaded parsers
|
|
68
|
+
const loadedParsers = new Map();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get the appropriate parser for a file extension
|
|
72
|
+
* @param {string} extension - File extension (with dot, e.g., '.java')
|
|
73
|
+
* @returns {Promise<Object|null>} - Parser module or null if not supported
|
|
74
|
+
*/
|
|
75
|
+
export async function getParser(extension) {
|
|
76
|
+
const normalizedExt = extension.toLowerCase();
|
|
77
|
+
const loader = parserRegistry[normalizedExt];
|
|
78
|
+
|
|
79
|
+
if (!loader) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check cache
|
|
84
|
+
if (loadedParsers.has(normalizedExt)) {
|
|
85
|
+
return loadedParsers.get(normalizedExt);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Load parser
|
|
89
|
+
try {
|
|
90
|
+
const module = await loader();
|
|
91
|
+
const parser = module.default || module;
|
|
92
|
+
loadedParsers.set(normalizedExt, parser);
|
|
93
|
+
return parser;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// Parser not implemented yet - return null
|
|
96
|
+
console.warn(`[ParserRegistry] Failed to load parser for ${extension}: ${error.message}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if a file extension is supported
|
|
103
|
+
* @param {string} extension - File extension (with dot)
|
|
104
|
+
* @returns {boolean} - True if supported
|
|
105
|
+
*/
|
|
106
|
+
export function isSupported(extension) {
|
|
107
|
+
return extension.toLowerCase() in parserRegistry;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get all supported extensions
|
|
112
|
+
* @returns {string[]} - Array of supported extensions
|
|
113
|
+
*/
|
|
114
|
+
export function getSupportedExtensions() {
|
|
115
|
+
return Object.keys(parserRegistry);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Parse a file using the appropriate parser
|
|
120
|
+
* @param {Object|string} file - File object with path/relativePath or just path string
|
|
121
|
+
* @param {Object} options - Parser options
|
|
122
|
+
* @returns {Promise<ParseResult|null>} - Parse result or null if unsupported
|
|
123
|
+
*/
|
|
124
|
+
export async function parseFile(file, options = {}) {
|
|
125
|
+
const filePath = typeof file === 'string' ? file : (file.path || file.relativePath);
|
|
126
|
+
const extension = extname(filePath);
|
|
127
|
+
|
|
128
|
+
const parser = await getParser(extension);
|
|
129
|
+
if (!parser) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Find the parse function
|
|
134
|
+
const parseFn = parser.parse || parser.parseFile || parser.parseJavaScript || parser.default;
|
|
135
|
+
if (typeof parseFn !== 'function') {
|
|
136
|
+
console.warn(`[ParserRegistry] No parse function found for ${extension}`);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
return await parseFn(file, options);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return {
|
|
144
|
+
file: { path: filePath, relativePath: filePath },
|
|
145
|
+
content: '',
|
|
146
|
+
functions: [],
|
|
147
|
+
classes: [],
|
|
148
|
+
exports: [],
|
|
149
|
+
imports: [],
|
|
150
|
+
annotations: [],
|
|
151
|
+
lines: 0,
|
|
152
|
+
size: 0,
|
|
153
|
+
parseMethod: 'error',
|
|
154
|
+
error: error.message
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Parse multiple files in parallel
|
|
161
|
+
* @param {Array<Object|string>} files - Array of file objects or paths
|
|
162
|
+
* @param {Object} options - Parser options
|
|
163
|
+
* @param {number} [options.concurrency=10] - Max concurrent parses
|
|
164
|
+
* @param {Function} [options.onProgress] - Progress callback
|
|
165
|
+
* @returns {Promise<ParseResult[]>} - Array of parse results
|
|
166
|
+
*/
|
|
167
|
+
export async function parseFiles(files, options = {}) {
|
|
168
|
+
const { concurrency = 10, onProgress } = options;
|
|
169
|
+
const results = [];
|
|
170
|
+
const total = files.length;
|
|
171
|
+
|
|
172
|
+
// Process in batches
|
|
173
|
+
for (let i = 0; i < files.length; i += concurrency) {
|
|
174
|
+
const batch = files.slice(i, i + concurrency);
|
|
175
|
+
const batchResults = await Promise.all(
|
|
176
|
+
batch.map(file => parseFile(file, options))
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
results.push(...batchResults.filter(r => r !== null));
|
|
180
|
+
|
|
181
|
+
if (onProgress) {
|
|
182
|
+
onProgress({ current: Math.min(i + concurrency, total), total });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return results;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get language info for a file extension
|
|
191
|
+
* @param {string} extension - File extension
|
|
192
|
+
* @returns {Object} - Language info
|
|
193
|
+
*/
|
|
194
|
+
export function getLanguageInfo(extension) {
|
|
195
|
+
const ext = extension.toLowerCase();
|
|
196
|
+
const languageMap = {
|
|
197
|
+
'.js': { name: 'JavaScript', family: 'js' },
|
|
198
|
+
'.mjs': { name: 'JavaScript', family: 'js' },
|
|
199
|
+
'.cjs': { name: 'JavaScript', family: 'js' },
|
|
200
|
+
'.jsx': { name: 'JSX', family: 'js' },
|
|
201
|
+
'.ts': { name: 'TypeScript', family: 'js' },
|
|
202
|
+
'.mts': { name: 'TypeScript', family: 'js' },
|
|
203
|
+
'.cts': { name: 'TypeScript', family: 'js' },
|
|
204
|
+
'.tsx': { name: 'TSX', family: 'js' },
|
|
205
|
+
'.vue': { name: 'Vue', family: 'js' },
|
|
206
|
+
'.svelte': { name: 'Svelte', family: 'js' },
|
|
207
|
+
'.java': { name: 'Java', family: 'jvm' },
|
|
208
|
+
'.kt': { name: 'Kotlin', family: 'jvm' },
|
|
209
|
+
'.kts': { name: 'Kotlin Script', family: 'jvm' },
|
|
210
|
+
'.scala': { name: 'Scala', family: 'jvm' },
|
|
211
|
+
'.cs': { name: 'C#', family: 'dotnet' },
|
|
212
|
+
'.fs': { name: 'F#', family: 'dotnet' },
|
|
213
|
+
'.vb': { name: 'Visual Basic', family: 'dotnet' },
|
|
214
|
+
'.py': { name: 'Python', family: 'python' },
|
|
215
|
+
'.pyi': { name: 'Python Stub', family: 'python' },
|
|
216
|
+
'.go': { name: 'Go', family: 'go' },
|
|
217
|
+
'.rs': { name: 'Rust', family: 'rust' },
|
|
218
|
+
'.rb': { name: 'Ruby', family: 'ruby' },
|
|
219
|
+
'.php': { name: 'PHP', family: 'php' },
|
|
220
|
+
'.swift': { name: 'Swift', family: 'swift' }
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
return languageMap[ext] || { name: 'Unknown', family: 'unknown' };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default {
|
|
227
|
+
getParser,
|
|
228
|
+
isSupported,
|
|
229
|
+
getSupportedExtensions,
|
|
230
|
+
parseFile,
|
|
231
|
+
parseFiles,
|
|
232
|
+
getLanguageInfo
|
|
233
|
+
};
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
// src/scanner/parsers/rust.mjs
|
|
2
|
+
// Rust parser with workspace and module support
|
|
3
|
+
|
|
4
|
+
import { readFileSync, existsSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse a Rust file and extract functions, structs, traits, impl blocks, 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 = []; // Structs, enums, traits
|
|
30
|
+
const exports = [];
|
|
31
|
+
const imports = [];
|
|
32
|
+
const mods = []; // Module declarations
|
|
33
|
+
|
|
34
|
+
// Track attributes for next item
|
|
35
|
+
let pendingAttributes = [];
|
|
36
|
+
|
|
37
|
+
// Extract use statements
|
|
38
|
+
const usePattern = /^\s*(?:pub\s+)?use\s+([\w:]+(?:::\{[^}]+\}|::\*)?)\s*;/gm;
|
|
39
|
+
let match;
|
|
40
|
+
while ((match = usePattern.exec(content)) !== null) {
|
|
41
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
42
|
+
imports.push({
|
|
43
|
+
module: match[1],
|
|
44
|
+
type: 'use',
|
|
45
|
+
line: lineNum
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Extract mod declarations (supports pub, pub(crate), pub(super), pub(in path))
|
|
50
|
+
// Also handles attributes before mod: #[macro_use] mod foo; #[cfg(...)] mod bar;
|
|
51
|
+
const modPattern = /^\s*(?:#\[[^\]]*\]\s*)*(?:pub(?:\([^)]+\))?\s+)?mod\s+(\w+)\s*[;{]/gm;
|
|
52
|
+
while ((match = modPattern.exec(content)) !== null) {
|
|
53
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
54
|
+
mods.push({
|
|
55
|
+
name: match[1],
|
|
56
|
+
public: match[0].includes('pub'),
|
|
57
|
+
line: lineNum
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Extract #[path = "..."] mod declarations — custom file paths for modules
|
|
62
|
+
// Pattern: #[path = "filename.rs"] mod name; or #[cfg_attr(..., path = "...")] mod name;
|
|
63
|
+
const pathModPattern = /^\s*#\[(?:cfg_attr\([^,]+,\s*)?path\s*=\s*"([^"]+)"\)?\]\s*(?:#\[[^\]]*\]\s*)*(?:pub(?:\([^)]+\))?\s+)?mod\s+(\w+)\s*[;{]/gm;
|
|
64
|
+
while ((match = pathModPattern.exec(content)) !== null) {
|
|
65
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
66
|
+
// Store path-remapped mod with the target filename
|
|
67
|
+
const existing = mods.find(m => m.name === match[2] && m.line === lineNum);
|
|
68
|
+
if (existing) {
|
|
69
|
+
existing.pathOverride = match[1]; // e.g., "unix.rs", "windows/mod.rs"
|
|
70
|
+
} else {
|
|
71
|
+
mods.push({
|
|
72
|
+
name: match[2],
|
|
73
|
+
public: match[0].includes('pub'),
|
|
74
|
+
line: lineNum,
|
|
75
|
+
pathOverride: match[1]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Parse line by line
|
|
81
|
+
for (let i = 0; i < lines.length; i++) {
|
|
82
|
+
const line = lines[i];
|
|
83
|
+
const lineNum = i + 1;
|
|
84
|
+
|
|
85
|
+
// Detect attributes (#[...])
|
|
86
|
+
const attrMatch = line.match(/^\s*#\[([^\]]+)\]/);
|
|
87
|
+
if (attrMatch) {
|
|
88
|
+
pendingAttributes.push({
|
|
89
|
+
name: attrMatch[1],
|
|
90
|
+
line: lineNum
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Detect function declaration
|
|
96
|
+
const funcMatch = line.match(/^\s*(pub(?:\([^)]+\))?\s+)?(async\s+)?fn\s+(\w+)(?:<[^>]+>)?\s*\(([^)]*)\)(?:\s*->\s*([\w<>,\s&']+))?/);
|
|
97
|
+
if (funcMatch) {
|
|
98
|
+
const funcInfo = {
|
|
99
|
+
name: funcMatch[3],
|
|
100
|
+
type: funcMatch[2] ? 'async function' : 'function',
|
|
101
|
+
visibility: funcMatch[1] ? 'public' : 'private',
|
|
102
|
+
async: !!funcMatch[2],
|
|
103
|
+
line: lineNum,
|
|
104
|
+
endLine: findBlockEnd(lines, i),
|
|
105
|
+
params: parseParams(funcMatch[4]),
|
|
106
|
+
returnType: funcMatch[5]?.trim() || null,
|
|
107
|
+
decorators: [...pendingAttributes],
|
|
108
|
+
attributes: [...pendingAttributes],
|
|
109
|
+
signature: `fn ${funcMatch[3]}(${funcMatch[4]})`,
|
|
110
|
+
exported: !!funcMatch[1]
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
funcInfo.lineCount = funcInfo.endLine - funcInfo.line + 1;
|
|
114
|
+
funcInfo.sizeBytes = extractCode(content, funcInfo.line, funcInfo.endLine).length;
|
|
115
|
+
|
|
116
|
+
// Check for main function
|
|
117
|
+
if (funcMatch[3] === 'main' && !funcMatch[1]) {
|
|
118
|
+
funcInfo.isMainFunction = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
functions.push(funcInfo);
|
|
122
|
+
pendingAttributes = [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Detect struct declaration
|
|
126
|
+
const structMatch = line.match(/^\s*(pub(?:\([^)]+\))?\s+)?struct\s+(\w+)(?:<[^>]+>)?/);
|
|
127
|
+
if (structMatch) {
|
|
128
|
+
const structInfo = {
|
|
129
|
+
name: structMatch[2],
|
|
130
|
+
type: 'struct',
|
|
131
|
+
visibility: structMatch[1] ? 'public' : 'private',
|
|
132
|
+
line: lineNum,
|
|
133
|
+
endLine: findBlockEnd(lines, i),
|
|
134
|
+
decorators: [...pendingAttributes],
|
|
135
|
+
attributes: [...pendingAttributes],
|
|
136
|
+
methods: [],
|
|
137
|
+
exported: !!structMatch[1]
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
structInfo.lineCount = structInfo.endLine - structInfo.line + 1;
|
|
141
|
+
structInfo.sizeBytes = extractCode(content, structInfo.line, structInfo.endLine).length;
|
|
142
|
+
|
|
143
|
+
classes.push(structInfo);
|
|
144
|
+
pendingAttributes = [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Detect enum declaration
|
|
148
|
+
const enumMatch = line.match(/^\s*(pub(?:\([^)]+\))?\s+)?enum\s+(\w+)(?:<[^>]+>)?/);
|
|
149
|
+
if (enumMatch) {
|
|
150
|
+
const enumInfo = {
|
|
151
|
+
name: enumMatch[2],
|
|
152
|
+
type: 'enum',
|
|
153
|
+
visibility: enumMatch[1] ? 'public' : 'private',
|
|
154
|
+
line: lineNum,
|
|
155
|
+
endLine: findBlockEnd(lines, i),
|
|
156
|
+
decorators: [...pendingAttributes],
|
|
157
|
+
attributes: [...pendingAttributes],
|
|
158
|
+
exported: !!enumMatch[1]
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
enumInfo.lineCount = enumInfo.endLine - enumInfo.line + 1;
|
|
162
|
+
enumInfo.sizeBytes = extractCode(content, enumInfo.line, enumInfo.endLine).length;
|
|
163
|
+
|
|
164
|
+
classes.push(enumInfo);
|
|
165
|
+
pendingAttributes = [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Detect trait declaration
|
|
169
|
+
const traitMatch = line.match(/^\s*(pub(?:\([^)]+\))?\s+)?trait\s+(\w+)(?:<[^>]+>)?/);
|
|
170
|
+
if (traitMatch) {
|
|
171
|
+
const traitInfo = {
|
|
172
|
+
name: traitMatch[2],
|
|
173
|
+
type: 'trait',
|
|
174
|
+
visibility: traitMatch[1] ? 'public' : 'private',
|
|
175
|
+
line: lineNum,
|
|
176
|
+
endLine: findBlockEnd(lines, i),
|
|
177
|
+
decorators: [...pendingAttributes],
|
|
178
|
+
attributes: [...pendingAttributes],
|
|
179
|
+
methods: [],
|
|
180
|
+
exported: !!traitMatch[1]
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
traitInfo.lineCount = traitInfo.endLine - traitInfo.line + 1;
|
|
184
|
+
traitInfo.sizeBytes = extractCode(content, traitInfo.line, traitInfo.endLine).length;
|
|
185
|
+
|
|
186
|
+
classes.push(traitInfo);
|
|
187
|
+
pendingAttributes = [];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Detect impl block
|
|
191
|
+
const implMatch = line.match(/^\s*impl(?:<[^>]+>)?\s+(?:(\w+)\s+for\s+)?(\w+)(?:<[^>]+>)?/);
|
|
192
|
+
if (implMatch && !line.includes('fn ')) {
|
|
193
|
+
const implInfo = {
|
|
194
|
+
name: implMatch[2],
|
|
195
|
+
trait: implMatch[1] || null,
|
|
196
|
+
type: 'impl',
|
|
197
|
+
line: lineNum,
|
|
198
|
+
endLine: findBlockEnd(lines, i),
|
|
199
|
+
methods: []
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Find the struct/enum this impl is for and add methods
|
|
203
|
+
const target = classes.find(c => c.name === implInfo.name);
|
|
204
|
+
if (target) {
|
|
205
|
+
// Parse impl methods would go here
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
pendingAttributes = [];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Clear pending attributes if we hit something else
|
|
212
|
+
if (line.trim() && !line.trim().startsWith('#[') && !line.trim().startsWith('//')) {
|
|
213
|
+
if (!funcMatch && !structMatch && !enumMatch && !traitMatch && !implMatch) {
|
|
214
|
+
pendingAttributes = [];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Determine exports (public items)
|
|
220
|
+
exports.push(
|
|
221
|
+
...functions.filter(f => f.exported).map(f => ({
|
|
222
|
+
name: f.name,
|
|
223
|
+
type: 'function',
|
|
224
|
+
line: f.line
|
|
225
|
+
})),
|
|
226
|
+
...classes.filter(c => c.exported).map(c => ({
|
|
227
|
+
name: c.name,
|
|
228
|
+
type: c.type,
|
|
229
|
+
line: c.line
|
|
230
|
+
}))
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Check if this is a main.rs or lib.rs
|
|
234
|
+
const isMainFile = relativePath.endsWith('main.rs');
|
|
235
|
+
const isLibFile = relativePath.endsWith('lib.rs');
|
|
236
|
+
const isModFile = relativePath.endsWith('mod.rs');
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
file: { path: filePath, relativePath },
|
|
240
|
+
content,
|
|
241
|
+
functions,
|
|
242
|
+
classes,
|
|
243
|
+
exports,
|
|
244
|
+
imports,
|
|
245
|
+
annotations: pendingAttributes, // Rust uses attributes instead of annotations
|
|
246
|
+
mods,
|
|
247
|
+
lines: lines.length,
|
|
248
|
+
size: content.length,
|
|
249
|
+
parseMethod: 'rust-regex',
|
|
250
|
+
metadata: {
|
|
251
|
+
hasMainFunction: functions.some(f => f.isMainFunction),
|
|
252
|
+
isMainFile,
|
|
253
|
+
isLibFile,
|
|
254
|
+
isModFile,
|
|
255
|
+
isBinaryCrate: isMainFile && functions.some(f => f.isMainFunction),
|
|
256
|
+
isLibraryCrate: isLibFile,
|
|
257
|
+
publicMods: mods.filter(m => m.public).map(m => m.name),
|
|
258
|
+
privateMods: mods.filter(m => !m.public).map(m => m.name)
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return createEmptyResult(filePath, relativePath, `Parse error: ${error.message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Parse function parameters
|
|
269
|
+
*/
|
|
270
|
+
function parseParams(paramsStr) {
|
|
271
|
+
if (!paramsStr || !paramsStr.trim()) return [];
|
|
272
|
+
|
|
273
|
+
const params = [];
|
|
274
|
+
let depth = 0;
|
|
275
|
+
let current = '';
|
|
276
|
+
|
|
277
|
+
for (const char of paramsStr) {
|
|
278
|
+
if (char === '<' || char === '(' || char === '[') depth++;
|
|
279
|
+
else if (char === '>' || char === ')' || char === ']') depth--;
|
|
280
|
+
else if (char === ',' && depth === 0) {
|
|
281
|
+
if (current.trim()) {
|
|
282
|
+
params.push(parseParam(current.trim()));
|
|
283
|
+
}
|
|
284
|
+
current = '';
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
current += char;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (current.trim()) {
|
|
291
|
+
params.push(parseParam(current.trim()));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return params;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Parse a single parameter
|
|
299
|
+
*/
|
|
300
|
+
function parseParam(paramStr) {
|
|
301
|
+
// Handle: name: Type, &self, &mut self, mut name: Type
|
|
302
|
+
if (paramStr === 'self' || paramStr === '&self' || paramStr === '&mut self') {
|
|
303
|
+
return { name: 'self', type: 'self', isSelf: true };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const match = paramStr.match(/^(mut\s+)?(\w+)\s*:\s*(.+)$/);
|
|
307
|
+
if (match) {
|
|
308
|
+
return {
|
|
309
|
+
name: match[2],
|
|
310
|
+
type: match[3].trim(),
|
|
311
|
+
mutable: !!match[1]
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
return { name: paramStr, type: null };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Find end of a code block (matching braces)
|
|
319
|
+
*/
|
|
320
|
+
function findBlockEnd(lines, startIndex) {
|
|
321
|
+
let braceCount = 0;
|
|
322
|
+
let started = false;
|
|
323
|
+
|
|
324
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
325
|
+
const line = lines[i];
|
|
326
|
+
|
|
327
|
+
let inString = false;
|
|
328
|
+
let stringChar = '';
|
|
329
|
+
|
|
330
|
+
for (let j = 0; j < line.length; j++) {
|
|
331
|
+
const char = line[j];
|
|
332
|
+
const nextChar = line[j + 1];
|
|
333
|
+
|
|
334
|
+
if (!inString && char === '"') {
|
|
335
|
+
inString = true;
|
|
336
|
+
stringChar = char;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (inString && char === stringChar && line[j - 1] !== '\\') {
|
|
340
|
+
inString = false;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (inString) continue;
|
|
344
|
+
|
|
345
|
+
if (char === '/' && nextChar === '/') break;
|
|
346
|
+
|
|
347
|
+
if (char === '{') {
|
|
348
|
+
braceCount++;
|
|
349
|
+
started = true;
|
|
350
|
+
} else if (char === '}') {
|
|
351
|
+
braceCount--;
|
|
352
|
+
if (started && braceCount === 0) {
|
|
353
|
+
return i + 1;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return startIndex + 1;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Extract code between line numbers
|
|
364
|
+
*/
|
|
365
|
+
function extractCode(content, startLine, endLine) {
|
|
366
|
+
const lines = content.split('\n');
|
|
367
|
+
return lines.slice(startLine - 1, endLine).join('\n');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Create empty result
|
|
372
|
+
*/
|
|
373
|
+
function createEmptyResult(filePath, relativePath, error) {
|
|
374
|
+
return {
|
|
375
|
+
file: { path: filePath, relativePath },
|
|
376
|
+
content: '',
|
|
377
|
+
functions: [],
|
|
378
|
+
classes: [],
|
|
379
|
+
exports: [],
|
|
380
|
+
imports: [],
|
|
381
|
+
annotations: [],
|
|
382
|
+
mods: [],
|
|
383
|
+
lines: 0,
|
|
384
|
+
size: 0,
|
|
385
|
+
error,
|
|
386
|
+
parseMethod: 'none'
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check if a Rust file is an entry point
|
|
392
|
+
*/
|
|
393
|
+
export function isEntryPoint(parseResult) {
|
|
394
|
+
// Binary crate with main function
|
|
395
|
+
if (parseResult.metadata?.isBinaryCrate) {
|
|
396
|
+
return { isEntry: true, reason: 'Is binary crate (main.rs with fn main)' };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Library crate entry
|
|
400
|
+
if (parseResult.metadata?.isLibraryCrate) {
|
|
401
|
+
return { isEntry: true, reason: 'Is library crate entry (lib.rs)' };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Test files
|
|
405
|
+
const hasTestAttr = parseResult.functions.some(f =>
|
|
406
|
+
f.attributes?.some(a => a.name.includes('test'))
|
|
407
|
+
);
|
|
408
|
+
if (hasTestAttr) {
|
|
409
|
+
return { isEntry: true, reason: 'Has #[test] functions' };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return { isEntry: false };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Check if module is publicly declared
|
|
417
|
+
*/
|
|
418
|
+
export function isPublicModule(parseResult, modName) {
|
|
419
|
+
const mod = parseResult.mods?.find(m => m.name === modName);
|
|
420
|
+
return mod?.public ?? false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export default {
|
|
424
|
+
parse,
|
|
425
|
+
isEntryPoint,
|
|
426
|
+
isPublicModule
|
|
427
|
+
};
|