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