@getmikk/core 1.8.2 → 1.9.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 (43) hide show
  1. package/package.json +3 -1
  2. package/src/constants.ts +285 -0
  3. package/src/contract/contract-generator.ts +7 -0
  4. package/src/contract/index.ts +2 -3
  5. package/src/contract/lock-compiler.ts +74 -42
  6. package/src/contract/lock-reader.ts +24 -4
  7. package/src/contract/schema.ts +27 -1
  8. package/src/error-handler.ts +430 -0
  9. package/src/graph/cluster-detector.ts +45 -20
  10. package/src/graph/confidence-engine.ts +60 -0
  11. package/src/graph/dead-code-detector.ts +27 -5
  12. package/src/graph/graph-builder.ts +298 -238
  13. package/src/graph/impact-analyzer.ts +131 -114
  14. package/src/graph/index.ts +4 -0
  15. package/src/graph/memory-manager.ts +345 -0
  16. package/src/graph/query-engine.ts +79 -0
  17. package/src/graph/risk-engine.ts +86 -0
  18. package/src/graph/types.ts +89 -64
  19. package/src/parser/boundary-checker.ts +3 -1
  20. package/src/parser/change-detector.ts +99 -0
  21. package/src/parser/go/go-extractor.ts +28 -9
  22. package/src/parser/go/go-parser.ts +2 -0
  23. package/src/parser/index.ts +88 -38
  24. package/src/parser/javascript/js-extractor.ts +1 -1
  25. package/src/parser/javascript/js-parser.ts +2 -0
  26. package/src/parser/oxc-parser.ts +675 -0
  27. package/src/parser/oxc-resolver.ts +83 -0
  28. package/src/parser/tree-sitter/parser.ts +27 -15
  29. package/src/parser/types.ts +100 -73
  30. package/src/parser/typescript/ts-extractor.ts +241 -537
  31. package/src/parser/typescript/ts-parser.ts +16 -171
  32. package/src/parser/typescript/ts-resolver.ts +11 -1
  33. package/src/search/bm25.ts +5 -2
  34. package/src/utils/minimatch.ts +1 -1
  35. package/tests/contract.test.ts +2 -2
  36. package/tests/dead-code.test.ts +7 -7
  37. package/tests/esm-resolver.test.ts +75 -0
  38. package/tests/graph.test.ts +20 -20
  39. package/tests/helpers.ts +11 -6
  40. package/tests/impact-classified.test.ts +37 -41
  41. package/tests/parser.test.ts +7 -5
  42. package/tests/ts-parser.test.ts +27 -52
  43. package/test-output.txt +0 -373
@@ -0,0 +1,675 @@
1
+ import path from 'node:path';
2
+ import { parseSync } from 'oxc-parser';
3
+ import { BaseParser } from './base-parser.js';
4
+ import { OxcResolver } from './oxc-resolver.js';
5
+ import { hashContent } from '../hash/file-hasher.js';
6
+ import type {
7
+ ParsedFile,
8
+ ParsedFunction,
9
+ ParsedClass,
10
+ ParsedVariable,
11
+ ParsedImport,
12
+ ParsedExport,
13
+ ParsedParam,
14
+ CallExpression,
15
+ ParsedGeneric
16
+ } from './types.js';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // LineIndex — O(log n) byte-offset → 1-based line number
20
+ // ---------------------------------------------------------------------------
21
+ class LineIndex {
22
+ private readonly offsets: number[];
23
+
24
+ constructor(content: string) {
25
+ this.offsets = [0];
26
+ let i = 0;
27
+ while ((i = content.indexOf('\n', i)) !== -1) {
28
+ this.offsets.push(++i);
29
+ }
30
+ }
31
+
32
+ getLine(offset: number): number {
33
+ let lo = 0;
34
+ let hi = this.offsets.length - 1;
35
+ while (lo <= hi) {
36
+ const mid = (lo + hi) >>> 1;
37
+ if (this.offsets[mid] <= offset) lo = mid + 1;
38
+ else hi = mid - 1;
39
+ }
40
+ return hi + 1; // 1-based
41
+ }
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // ID allocation
46
+ // ---------------------------------------------------------------------------
47
+ // Canonical ID format (lowercased for stable matching):
48
+ // fn:<absolute-posix-path>:<functionname>
49
+ // fn:<absolute-posix-path>:<functionname>#2 (second occurrence in same file)
50
+ // class:<absolute-posix-path>:<classname>
51
+ // type:<absolute-posix-path>:<typename>
52
+ // enum:<absolute-posix-path>:<enumname>
53
+ // var:<absolute-posix-path>:<varname>
54
+ // prop:<absolute-posix-path>:<propname>
55
+ // ---------------------------------------------------------------------------
56
+ function makeAllocator(filePath: string): (prefix: string, name: string) => string {
57
+ const counter = new Map<string, number>();
58
+ const normalizedPath = filePath.replace(/\\/g, '/');
59
+ return (prefix: string, name: string): string => {
60
+ const key = `${prefix}:${name}`;
61
+ const count = (counter.get(key) ?? 0) + 1;
62
+ counter.set(key, count);
63
+ const suffix = count === 1 ? '' : `#${count}`;
64
+ return `${prefix}:${normalizedPath}:${name}${suffix}`.toLowerCase();
65
+ };
66
+ }
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Export detection helpers
70
+ // ---------------------------------------------------------------------------
71
+ function isDirectlyExported(parent: any): boolean {
72
+ return parent != null && (
73
+ parent.type === 'ExportNamedDeclaration' ||
74
+ parent.type === 'ExportDeclaration'
75
+ );
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // AST helpers
80
+ // ---------------------------------------------------------------------------
81
+ const memberExpressionTypes = new Set([
82
+ 'MemberExpression',
83
+ 'StaticMemberExpression',
84
+ 'ComputedMemberExpression',
85
+ 'OptionalMemberExpression',
86
+ ]);
87
+
88
+ function normalizeCallee(node: any): any {
89
+ if (!node || typeof node !== 'object') return null;
90
+ if (node.type === 'ChainExpression') return normalizeCallee(node.expression);
91
+ if (node.type === 'OptionalCallExpression') return normalizeCallee(node.callee);
92
+ return node;
93
+ }
94
+
95
+ function resolvePropertyName(node: any): string | null {
96
+ if (!node) return null;
97
+ if (node.type === 'Identifier') return node.name ?? null;
98
+ if (node.type === 'PrivateIdentifier') return `#${node.name}`;
99
+ if (node.type === 'Literal' || node.type === 'StringLiteral' || node.type === 'NumericLiteral') {
100
+ return node.value != null ? String(node.value) : node.raw ?? null;
101
+ }
102
+ return null;
103
+ }
104
+
105
+ function resolveObjectName(node: any): string | null {
106
+ if (!node) return null;
107
+ if (node.type === 'Identifier') return node.name;
108
+ if (node.type === 'ThisExpression') return 'this';
109
+ if (node.type === 'Super') return 'super';
110
+ if (memberExpressionTypes.has(node.type)) {
111
+ const parent = resolveObjectName(node.object ?? node.expression);
112
+ const prop = resolvePropertyName(node.property ?? node.expression);
113
+ if (parent && prop) return `${parent}.${prop}`;
114
+ return prop;
115
+ }
116
+ if (node.type === 'NewExpression' || node.type === 'CallExpression') {
117
+ return resolveObjectName(node.callee);
118
+ }
119
+ if (node.type === 'ChainExpression' || node.type === 'OptionalCallExpression') {
120
+ return resolveObjectName(node.expression ?? node.callee);
121
+ }
122
+ return null;
123
+ }
124
+
125
+ function resolveCallIdentity(callee: any): { name: string | null; type: CallExpression['type'] } {
126
+ const normalized = normalizeCallee(callee);
127
+ if (!normalized) return { name: null, type: 'function' };
128
+ if (normalized.type === 'Identifier') {
129
+ return { name: normalized.name ?? null, type: 'function' };
130
+ }
131
+ if (memberExpressionTypes.has(normalized.type)) {
132
+ const objName = resolveObjectName(normalized.object ?? normalized.expression);
133
+ const propName = resolvePropertyName(normalized.property ?? normalized.expression);
134
+ if (objName && propName) {
135
+ return { name: `${objName}.${propName}`, type: 'method' };
136
+ }
137
+ if (propName) {
138
+ return { name: propName, type: 'method' };
139
+ }
140
+ return { name: null, type: 'function' };
141
+ }
142
+ if (normalized.type === 'Super') {
143
+ return { name: 'super', type: 'method' };
144
+ }
145
+ if (normalized.type === 'CallExpression' || normalized.type === 'NewExpression') {
146
+ return resolveCallIdentity(normalized.callee);
147
+ }
148
+ return { name: null, type: 'function' };
149
+ }
150
+
151
+ function flattenPatternNames(pattern: any): string[] {
152
+ if (!pattern) return [];
153
+ switch (pattern.type) {
154
+ case 'Identifier':
155
+ return pattern.name ? [pattern.name] : ['unknown'];
156
+ case 'PrivateIdentifier':
157
+ return pattern.name ? [`#${pattern.name}`] : ['unknown'];
158
+ case 'AssignmentPattern':
159
+ return flattenPatternNames(pattern.left ?? pattern.argument ?? pattern.parameter);
160
+ case 'RestElement':
161
+ return flattenPatternNames(pattern.argument ?? pattern.value);
162
+ case 'TSParameterProperty':
163
+ return flattenPatternNames(pattern.parameter);
164
+ case 'ObjectPattern':
165
+ return (pattern.properties ?? []).flatMap((prop: any) => {
166
+ if (!prop) return [];
167
+ if (prop.type === 'RestElement') return flattenPatternNames(prop.argument);
168
+ return flattenPatternNames(prop.value ?? prop.key);
169
+ });
170
+ case 'ArrayPattern':
171
+ return (pattern.elements ?? []).flatMap((el: any) => flattenPatternNames(el));
172
+ case 'Property':
173
+ return flattenPatternNames(pattern.value ?? pattern.key);
174
+ default:
175
+ return ['unknown'];
176
+ }
177
+ }
178
+
179
+ function normalizeParamNode(node: any): any {
180
+ if (!node) return null;
181
+ if (node.type === 'TSParameterProperty') return normalizeParamNode(node.parameter);
182
+ return node;
183
+ }
184
+
185
+ function describeParamPattern(pattern: any): string {
186
+ if (!pattern) return 'unknown';
187
+ switch (pattern.type) {
188
+ case 'Identifier':
189
+ return pattern.name ?? 'unknown';
190
+ case 'PrivateIdentifier':
191
+ return pattern.name ? `#${pattern.name}` : 'unknown';
192
+ case 'AssignmentPattern':
193
+ return describeParamPattern(pattern.left ?? pattern.argument ?? pattern.parameter);
194
+ case 'RestElement':
195
+ return `...${describeParamPattern(pattern.argument ?? pattern.value ?? pattern.parameter)}`;
196
+ case 'ObjectPattern':
197
+ return '{...}';
198
+ case 'ArrayPattern':
199
+ return '[...]';
200
+ default:
201
+ return 'unknown';
202
+ }
203
+ }
204
+
205
+ function extractTypeParameterNames(typeParameters: any): string[] {
206
+ const params = typeParameters?.params ?? typeParameters?.parameters ?? [];
207
+ if (!Array.isArray(params)) return [];
208
+ return params.map((param: any) => param?.name?.name ?? 'unknown');
209
+ }
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Call extraction
213
+ // Captures direct calls (foo()) and method calls (obj.method(), this.method())
214
+ // Returns name === 'unknown' only when genuinely unresolvable; those are filtered.
215
+ // ---------------------------------------------------------------------------
216
+ function extractCalls(node: any, lineIndex: LineIndex): CallExpression[] {
217
+ const calls: CallExpression[] = [];
218
+
219
+ const walk = (n: any): void => {
220
+ if (!n || typeof n !== 'object') return;
221
+
222
+ if (n.type === 'CallExpression' && n.span) {
223
+ const { name, type } = resolveCallIdentity(n.callee);
224
+ if (name) {
225
+ calls.push({
226
+ name,
227
+ line: lineIndex.getLine(n.span.start),
228
+ type,
229
+ });
230
+ }
231
+ }
232
+
233
+ for (const key of Object.keys(n)) {
234
+ if (key === 'span' || key === 'type') continue;
235
+ const child = n[key];
236
+ if (Array.isArray(child)) {
237
+ for (const c of child) walk(c);
238
+ } else if (child && typeof child === 'object') {
239
+ walk(child);
240
+ }
241
+ }
242
+ };
243
+
244
+ walk(node);
245
+ return calls;
246
+ }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Parameter extraction
250
+ // ---------------------------------------------------------------------------
251
+ function extractParams(params: any[]): ParsedParam[] {
252
+ return params.map(p => {
253
+ const normalized = normalizeParamNode(p);
254
+ const pattern = normalized?.pattern ?? normalized?.left ?? normalized?.argument ?? normalized;
255
+ const name = describeParamPattern(pattern);
256
+ const optional = !!normalized?.optional || pattern?.type === 'AssignmentPattern' || pattern?.type === 'RestElement';
257
+ const hasDefault = pattern?.type === 'AssignmentPattern' || normalized?.defaultValue != null || normalized?.initializer != null;
258
+ return {
259
+ name,
260
+ type: 'any',
261
+ optional,
262
+ defaultValue: hasDefault ? 'default' : undefined,
263
+ };
264
+ });
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Span helper
269
+ // ---------------------------------------------------------------------------
270
+ function getSpan(node: any): { start: number; end: number } {
271
+ const s = node?.span ?? node ?? {};
272
+ return { start: s.start ?? 0, end: s.end ?? 0 };
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // OxcParser
277
+ // ---------------------------------------------------------------------------
278
+ export class OxcParser extends BaseParser {
279
+ public async parse(filePath: string, content: string): Promise<ParsedFile> {
280
+ const ext = path.extname(filePath).toLowerCase();
281
+ const isTS = ['.ts', '.tsx', '.mts', '.cts'].includes(ext);
282
+
283
+ let ast: any;
284
+ try {
285
+ const result = parseSync(filePath, content, {
286
+ sourceType: 'module',
287
+ lang: isTS ? 'ts' : 'js',
288
+ });
289
+ ast = result.program;
290
+ } catch {
291
+ // Return empty file on parse error — never crash the pipeline
292
+ return this.emptyFile(filePath, content, isTS);
293
+ }
294
+
295
+ const lineIndex = new LineIndex(content);
296
+ const allocateId = makeAllocator(filePath);
297
+ const normalizedFilePath = filePath.replace(/\\/g, '/');
298
+
299
+ const functions: ParsedFunction[] = [];
300
+ const classes: ParsedClass[] = [];
301
+ const variables: ParsedVariable[] = [];
302
+ const generics: ParsedGeneric[] = [];
303
+ const imports: ParsedImport[] = [];
304
+ const exports: ParsedExport[] = [];
305
+ const moduleCalls: CallExpression[] = [];
306
+
307
+ const visit = (node: any, parent: any = null): void => {
308
+ if (!node || typeof node !== 'object') return;
309
+
310
+ switch (node.type) {
311
+
312
+ // ── Imports ────────────────────────────────────────────────
313
+ case 'ImportDeclaration': {
314
+ if (node.importKind === 'type') break;
315
+ const names: string[] = [];
316
+ let isDefault = false;
317
+ for (const spec of node.specifiers ?? []) {
318
+ if (spec.importKind === 'type') continue;
319
+ if (spec.type === 'ImportDefaultSpecifier') {
320
+ isDefault = true;
321
+ }
322
+ if (spec.local?.name) names.push(spec.local.name);
323
+ }
324
+ imports.push({
325
+ source: node.source.value,
326
+ resolvedPath: '',
327
+ names,
328
+ isDefault,
329
+ isDynamic: false,
330
+ });
331
+ break;
332
+ }
333
+
334
+ // ── Dynamic import() ───────────────────────────────────────
335
+ case 'ImportExpression': {
336
+ const arg = node.source ?? node.arguments?.[0];
337
+ if (arg?.type === 'StringLiteral' || arg?.type === 'Literal') {
338
+ imports.push({
339
+ source: arg.value,
340
+ resolvedPath: '',
341
+ names: [],
342
+ isDefault: false,
343
+ isDynamic: true,
344
+ });
345
+ }
346
+ break;
347
+ }
348
+
349
+ // ── Function Declaration ───────────────────────────────────
350
+ case 'FunctionDeclaration': {
351
+ if (!node.id) break;
352
+ const name = node.id.name;
353
+ const span = getSpan(node);
354
+ const exported = isDirectlyExported(parent);
355
+ functions.push({
356
+ id: allocateId('fn', name),
357
+ name,
358
+ file: normalizedFilePath,
359
+ startLine: lineIndex.getLine(span.start),
360
+ endLine: lineIndex.getLine(span.end),
361
+ params: extractParams(node.params?.items ?? node.params ?? []),
362
+ returnType: 'void',
363
+ isExported: exported,
364
+ isAsync: !!node.async,
365
+ calls: extractCalls(node.body ?? node, lineIndex),
366
+ hash: hashContent(JSON.stringify(node.body ?? {})),
367
+ purpose: '',
368
+ edgeCasesHandled: [],
369
+ errorHandling: [],
370
+ detailedLines: [],
371
+ });
372
+ if (exported) exports.push({ name, type: 'function', file: normalizedFilePath });
373
+ break;
374
+ }
375
+
376
+ // ── Class Declaration ──────────────────────────────────────
377
+ case 'ClassDeclaration': {
378
+ if (!node.id) break;
379
+ const name = node.id.name;
380
+ const span = getSpan(node);
381
+ const exported = isDirectlyExported(parent);
382
+ const methods: ParsedFunction[] = [];
383
+ const properties: ParsedVariable[] = [];
384
+
385
+ for (const member of node.body?.body ?? []) {
386
+ if (member.type === 'MethodDefinition' || member.type === 'PropertyDefinition') {
387
+ const key = member.key;
388
+ if (!key) continue;
389
+ const mName = key.type === 'Identifier' ? key.name :
390
+ key.type === 'PrivateIdentifier' ? `#${key.name}` :
391
+ null;
392
+ if (!mName) continue;
393
+
394
+ if (member.type === 'MethodDefinition') {
395
+ const value = member.value;
396
+ const mSpan = getSpan(member);
397
+ methods.push({
398
+ id: allocateId('fn', `${name}.${mName}`),
399
+ name: `${name}.${mName}`,
400
+ file: normalizedFilePath,
401
+ startLine: lineIndex.getLine(mSpan.start),
402
+ endLine: lineIndex.getLine(mSpan.end),
403
+ params: extractParams(value?.params?.items ?? value?.params ?? []),
404
+ returnType: 'any',
405
+ isExported: exported,
406
+ isAsync: !!value?.async,
407
+ calls: extractCalls(value?.body ?? value ?? {}, lineIndex),
408
+ hash: hashContent(JSON.stringify(value?.body ?? {})),
409
+ purpose: '',
410
+ edgeCasesHandled: [],
411
+ errorHandling: [],
412
+ detailedLines: [],
413
+ });
414
+ } else {
415
+ // PropertyDefinition
416
+ const pSpan = getSpan(member);
417
+ const propertyNode: ParsedVariable = {
418
+ id: allocateId('prop', `${name}.${mName}`),
419
+ name: `${name}.${mName}`,
420
+ type: 'any',
421
+ file: normalizedFilePath,
422
+ line: lineIndex.getLine(pSpan.start),
423
+ isExported: false,
424
+ isStatic: !!member.static,
425
+ };
426
+ properties.push(propertyNode);
427
+ variables.push(propertyNode);
428
+ }
429
+ }
430
+ }
431
+
432
+ classes.push({
433
+ id: allocateId('class', name),
434
+ name,
435
+ file: normalizedFilePath,
436
+ startLine: lineIndex.getLine(span.start),
437
+ endLine: lineIndex.getLine(span.end),
438
+ methods,
439
+ properties,
440
+ extends: node.superClass?.name,
441
+ isExported: exported,
442
+ hash: hashContent(JSON.stringify(node.body ?? {})),
443
+ purpose: '',
444
+ });
445
+ if (exported) exports.push({ name, type: 'class', file: normalizedFilePath });
446
+ break;
447
+ }
448
+
449
+ // ── TS Type / Interface ────────────────────────────────────
450
+ case 'TSTypeAliasDeclaration':
451
+ case 'TSInterfaceDeclaration': {
452
+ if (!node.id) break;
453
+ const name = node.id.name;
454
+ const span = getSpan(node);
455
+ const kind = node.type === 'TSInterfaceDeclaration' ? 'interface' : 'type';
456
+ const exported = isDirectlyExported(parent);
457
+ const typeParameters = extractTypeParameterNames(node.typeParameters);
458
+ generics.push({
459
+ id: allocateId('type', name),
460
+ name,
461
+ type: kind,
462
+ file: normalizedFilePath,
463
+ startLine: lineIndex.getLine(span.start),
464
+ endLine: lineIndex.getLine(span.end),
465
+ isExported: exported,
466
+ typeParameters,
467
+ hash: hashContent(JSON.stringify(node)),
468
+ purpose: '',
469
+ });
470
+ if (exported) exports.push({ name, type: kind as any, file: normalizedFilePath });
471
+ break;
472
+ }
473
+
474
+ // ── TS Enum ────────────────────────────────────────────────
475
+ case 'TSEnumDeclaration': {
476
+ if (!node.id) break;
477
+ const name = node.id.name;
478
+ const span = getSpan(node);
479
+ const exported = isDirectlyExported(parent);
480
+ generics.push({
481
+ id: allocateId('enum', name),
482
+ name,
483
+ type: 'enum',
484
+ file: normalizedFilePath,
485
+ startLine: lineIndex.getLine(span.start),
486
+ endLine: lineIndex.getLine(span.end),
487
+ isExported: exported,
488
+ hash: hashContent(JSON.stringify(node)),
489
+ purpose: '',
490
+ });
491
+ if (exported) exports.push({ name, type: 'const', file: normalizedFilePath });
492
+ break;
493
+ }
494
+
495
+ // ── Variable Declaration ───────────────────────────────────
496
+ case 'VariableDeclaration': {
497
+ const exported = isDirectlyExported(parent);
498
+ for (const decl of node.declarations ?? []) {
499
+ const variableNames = flattenPatternNames(decl.id);
500
+ if (variableNames.length === 0) continue;
501
+
502
+ // Unwrap TS expressions to find the real initializer
503
+ let init = decl.init;
504
+ while (init && (
505
+ init.type === 'TSAsExpression' ||
506
+ init.type === 'TSSatisfiesExpression' ||
507
+ init.type === 'ParenthesizedExpression' ||
508
+ init.type === 'TypeAssertion' ||
509
+ init.type === 'TSNonNullExpression' ||
510
+ init.type === 'TSInstantiationExpression'
511
+ )) {
512
+ init = init.expression;
513
+ }
514
+
515
+ const isFn = init && (
516
+ init.type === 'FunctionExpression' ||
517
+ init.type === 'ArrowFunctionExpression'
518
+ );
519
+
520
+ if (isFn && variableNames.length === 1) {
521
+ const name = variableNames[0];
522
+ const span = getSpan(init) ?? getSpan(decl);
523
+ functions.push({
524
+ id: allocateId('fn', name),
525
+ name,
526
+ file: normalizedFilePath,
527
+ startLine: lineIndex.getLine(span.start),
528
+ endLine: lineIndex.getLine(span.end),
529
+ params: extractParams(init.params?.items ?? init.params ?? []),
530
+ returnType: 'any',
531
+ isExported: exported,
532
+ isAsync: !!init.async,
533
+ calls: extractCalls(init.body ?? init, lineIndex),
534
+ hash: hashContent(JSON.stringify(init.body ?? {})),
535
+ purpose: '',
536
+ edgeCasesHandled: [],
537
+ errorHandling: [],
538
+ detailedLines: [],
539
+ });
540
+ if (exported) exports.push({ name, type: 'function', file: normalizedFilePath });
541
+ } else {
542
+ const span = getSpan(decl);
543
+ const line = lineIndex.getLine(span.start);
544
+ for (const name of variableNames) {
545
+ if (!name) continue;
546
+ const variableNode: ParsedVariable = {
547
+ id: allocateId('var', name),
548
+ name,
549
+ type: 'any',
550
+ file: normalizedFilePath,
551
+ line,
552
+ isExported: exported,
553
+ };
554
+ variables.push(variableNode);
555
+ if (exported) exports.push({ name, type: 'variable', file: normalizedFilePath });
556
+ }
557
+ }
558
+ }
559
+ break;
560
+ }
561
+
562
+ // ── Export Default ─────────────────────────────────────────
563
+ case 'ExportDefaultDeclaration': {
564
+ const decl = node.declaration;
565
+ if (!decl) break;
566
+
567
+ if (decl.type === 'FunctionDeclaration' || decl.type === 'FunctionExpression' || decl.type === 'ArrowFunctionExpression') {
568
+ const name = decl.id?.name ?? 'default';
569
+ const span = getSpan(node);
570
+ functions.push({
571
+ id: allocateId('fn', name),
572
+ name,
573
+ file: normalizedFilePath,
574
+ startLine: lineIndex.getLine(span.start),
575
+ endLine: lineIndex.getLine(span.end),
576
+ params: extractParams(decl.params?.items ?? decl.params ?? []),
577
+ returnType: 'any',
578
+ isExported: true,
579
+ isAsync: !!decl.async,
580
+ calls: extractCalls(decl.body ?? decl, lineIndex),
581
+ hash: hashContent(JSON.stringify(decl.body ?? {})),
582
+ purpose: '',
583
+ edgeCasesHandled: [],
584
+ errorHandling: [],
585
+ detailedLines: [],
586
+ });
587
+ exports.push({ name, type: 'default', file: normalizedFilePath });
588
+ } else if (decl.type === 'ClassDeclaration' && decl.id) {
589
+ exports.push({ name: decl.id.name, type: 'default', file: normalizedFilePath });
590
+ } else if (decl.type === 'Identifier') {
591
+ exports.push({ name: decl.name, type: 'default', file: normalizedFilePath });
592
+ }
593
+ break;
594
+ }
595
+
596
+ // ── Named Exports ──────────────────────────────────────────
597
+ case 'ExportNamedDeclaration': {
598
+ // Re-export specifiers: export { foo, bar }
599
+ for (const spec of node.specifiers ?? []) {
600
+ if (spec.exported?.name) {
601
+ exports.push({ name: spec.exported.name, type: 'variable', file: normalizedFilePath });
602
+ }
603
+ }
604
+ // Declaration is handled by the declaration's own case with parent context
605
+ break;
606
+ }
607
+
608
+ // ── Module-level call expressions ─────────────────────────
609
+ case 'ExpressionStatement': {
610
+ if (node.expression?.type === 'CallExpression') {
611
+ const calls = extractCalls(node.expression, lineIndex);
612
+ moduleCalls.push(...calls);
613
+ }
614
+ break;
615
+ }
616
+ }
617
+
618
+ // Recurse into children
619
+ for (const key of Object.keys(node)) {
620
+ if (key === 'span' || key === 'type') continue;
621
+ const child = node[key];
622
+ if (Array.isArray(child)) {
623
+ for (const c of child) {
624
+ if (c && typeof c === 'object') visit(c, node);
625
+ }
626
+ } else if (child && typeof child === 'object') {
627
+ visit(child, node);
628
+ }
629
+ }
630
+ };
631
+
632
+ visit(ast);
633
+
634
+ return {
635
+ path: normalizedFilePath,
636
+ language: isTS ? 'typescript' : 'javascript',
637
+ functions,
638
+ classes,
639
+ variables,
640
+ generics,
641
+ imports,
642
+ exports,
643
+ routes: [],
644
+ calls: moduleCalls,
645
+ hash: hashContent(content),
646
+ parsedAt: Date.now(),
647
+ };
648
+ }
649
+
650
+ public resolveImports(files: ParsedFile[], projectRoot: string): ParsedFile[] {
651
+ const resolver = new OxcResolver(projectRoot);
652
+ return resolver.resolveBatch(files);
653
+ }
654
+
655
+ public getSupportedExtensions(): string[] {
656
+ return ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
657
+ }
658
+
659
+ private emptyFile(filePath: string, content: string, isTS: boolean): ParsedFile {
660
+ return {
661
+ path: filePath.replace(/\\/g, '/'),
662
+ language: isTS ? 'typescript' : 'javascript',
663
+ functions: [],
664
+ classes: [],
665
+ variables: [],
666
+ generics: [],
667
+ imports: [],
668
+ exports: [],
669
+ routes: [],
670
+ calls: [],
671
+ hash: hashContent(content),
672
+ parsedAt: Date.now(),
673
+ };
674
+ }
675
+ }