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