driftdetect-core 0.8.1 → 0.8.3

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 (41) hide show
  1. package/dist/call-graph/store/call-graph-store.d.ts +13 -1
  2. package/dist/call-graph/store/call-graph-store.d.ts.map +1 -1
  3. package/dist/call-graph/store/call-graph-store.js +164 -1
  4. package/dist/call-graph/store/call-graph-store.js.map +1 -1
  5. package/dist/index.d.ts +8 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +24 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/java/index.d.ts +8 -0
  10. package/dist/java/index.d.ts.map +1 -0
  11. package/dist/java/index.js +7 -0
  12. package/dist/java/index.js.map +1 -0
  13. package/dist/java/java-analyzer.d.ts +142 -0
  14. package/dist/java/java-analyzer.d.ts.map +1 -0
  15. package/dist/java/java-analyzer.js +515 -0
  16. package/dist/java/java-analyzer.js.map +1 -0
  17. package/dist/php/index.d.ts +8 -0
  18. package/dist/php/index.d.ts.map +1 -0
  19. package/dist/php/index.js +7 -0
  20. package/dist/php/index.js.map +1 -0
  21. package/dist/php/php-analyzer.d.ts +149 -0
  22. package/dist/php/php-analyzer.d.ts.map +1 -0
  23. package/dist/php/php-analyzer.js +546 -0
  24. package/dist/php/php-analyzer.js.map +1 -0
  25. package/dist/python/index.d.ts +8 -0
  26. package/dist/python/index.d.ts.map +1 -0
  27. package/dist/python/index.js +7 -0
  28. package/dist/python/index.js.map +1 -0
  29. package/dist/python/python-analyzer.d.ts +156 -0
  30. package/dist/python/python-analyzer.d.ts.map +1 -0
  31. package/dist/python/python-analyzer.js +535 -0
  32. package/dist/python/python-analyzer.js.map +1 -0
  33. package/dist/typescript/index.d.ts +13 -0
  34. package/dist/typescript/index.d.ts.map +1 -0
  35. package/dist/typescript/index.js +12 -0
  36. package/dist/typescript/index.js.map +1 -0
  37. package/dist/typescript/typescript-analyzer.d.ts +194 -0
  38. package/dist/typescript/typescript-analyzer.d.ts.map +1 -0
  39. package/dist/typescript/typescript-analyzer.js +762 -0
  40. package/dist/typescript/typescript-analyzer.js.map +1 -0
  41. package/package.json +1 -1
@@ -0,0 +1,762 @@
1
+ /**
2
+ * TypeScript/JavaScript Analyzer
3
+ *
4
+ * Main analyzer for TypeScript/JavaScript projects. Provides comprehensive analysis of:
5
+ * - HTTP routes (Express, NestJS, Fastify, Next.js)
6
+ * - React components and hooks
7
+ * - Error handling patterns
8
+ * - Data access patterns (Prisma, TypeORM, Drizzle, Sequelize)
9
+ * - Async patterns
10
+ * - Decorators (NestJS)
11
+ */
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import { createTypeScriptHybridExtractor } from '../call-graph/extractors/typescript-hybrid-extractor.js';
15
+ // ============================================================================
16
+ // Default Configuration
17
+ // ============================================================================
18
+ const DEFAULT_CONFIG = {
19
+ verbose: false,
20
+ includePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
21
+ excludePatterns: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**'],
22
+ };
23
+ const TS_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts'];
24
+ const JS_EXTENSIONS = ['.js', '.jsx', '.mjs', '.cjs'];
25
+ const ALL_EXTENSIONS = [...TS_EXTENSIONS, ...JS_EXTENSIONS];
26
+ // ============================================================================
27
+ // TypeScript Analyzer Implementation
28
+ // ============================================================================
29
+ export class TypeScriptAnalyzer {
30
+ config;
31
+ extractor;
32
+ constructor(config) {
33
+ this.config = { ...DEFAULT_CONFIG, ...config };
34
+ this.extractor = createTypeScriptHybridExtractor();
35
+ }
36
+ /**
37
+ * Full project analysis
38
+ */
39
+ async analyze() {
40
+ const startTime = Date.now();
41
+ const files = await this.findFiles();
42
+ const packageJson = await this.parsePackageJson();
43
+ const allFunctions = [];
44
+ const allClasses = [];
45
+ const allCalls = [];
46
+ const allImports = [];
47
+ const detectedFrameworks = new Set();
48
+ let linesOfCode = 0;
49
+ let testFileCount = 0;
50
+ let tsFiles = 0;
51
+ let jsFiles = 0;
52
+ let componentCount = 0;
53
+ let hookCount = 0;
54
+ let asyncFunctionCount = 0;
55
+ let decoratorCount = 0;
56
+ for (const file of files) {
57
+ const source = await fs.promises.readFile(file, 'utf-8');
58
+ linesOfCode += source.split('\n').length;
59
+ const ext = path.extname(file);
60
+ if (TS_EXTENSIONS.includes(ext))
61
+ tsFiles++;
62
+ if (JS_EXTENSIONS.includes(ext))
63
+ jsFiles++;
64
+ const isTestFile = this.isTestFile(file);
65
+ if (isTestFile)
66
+ testFileCount++;
67
+ const result = this.extractor.extract(source, file);
68
+ // Detect frameworks from imports
69
+ for (const imp of result.imports) {
70
+ const framework = this.detectFramework(imp.source);
71
+ if (framework)
72
+ detectedFrameworks.add(framework);
73
+ }
74
+ // Count components
75
+ componentCount += this.countComponents(result.functions, result.classes, source);
76
+ // Count hooks
77
+ hookCount += this.countHooks(result.calls);
78
+ // Count async functions
79
+ asyncFunctionCount += result.functions.filter(f => f.isAsync).length;
80
+ // Count decorators
81
+ decoratorCount += result.functions.reduce((sum, f) => sum + f.decorators.length, 0);
82
+ allFunctions.push(...result.functions);
83
+ allClasses.push(...result.classes);
84
+ allCalls.push(...result.calls);
85
+ allImports.push(...result.imports);
86
+ }
87
+ const analysisTimeMs = Date.now() - startTime;
88
+ return {
89
+ projectInfo: {
90
+ name: packageJson.name,
91
+ version: packageJson.version,
92
+ hasTypeScript: tsFiles > 0,
93
+ hasJavaScript: jsFiles > 0,
94
+ files: files.length,
95
+ tsFiles,
96
+ jsFiles,
97
+ },
98
+ detectedFrameworks: Array.from(detectedFrameworks),
99
+ stats: {
100
+ fileCount: files.length,
101
+ functionCount: allFunctions.length,
102
+ classCount: allClasses.length,
103
+ componentCount,
104
+ hookCount,
105
+ asyncFunctionCount,
106
+ decoratorCount,
107
+ linesOfCode,
108
+ testFileCount,
109
+ analysisTimeMs,
110
+ },
111
+ functions: allFunctions,
112
+ classes: allClasses,
113
+ calls: allCalls,
114
+ imports: allImports,
115
+ };
116
+ }
117
+ /**
118
+ * Analyze HTTP routes
119
+ */
120
+ async analyzeRoutes() {
121
+ const files = await this.findFiles();
122
+ const routes = [];
123
+ for (const file of files) {
124
+ const source = await fs.promises.readFile(file, 'utf-8');
125
+ const fileRoutes = this.extractRoutes(source, file);
126
+ routes.push(...fileRoutes);
127
+ }
128
+ const byFramework = {};
129
+ for (const route of routes) {
130
+ byFramework[route.framework] = (byFramework[route.framework] || 0) + 1;
131
+ }
132
+ return { routes, byFramework };
133
+ }
134
+ /**
135
+ * Analyze React components
136
+ */
137
+ async analyzeComponents() {
138
+ const files = await this.findFiles();
139
+ const components = [];
140
+ for (const file of files) {
141
+ const source = await fs.promises.readFile(file, 'utf-8');
142
+ const result = this.extractor.extract(source, file);
143
+ const fileComponents = this.extractComponents(result.functions, result.classes, source, file);
144
+ components.push(...fileComponents);
145
+ }
146
+ const byType = {
147
+ functional: components.filter(c => c.type === 'functional').length,
148
+ class: components.filter(c => c.type === 'class').length,
149
+ };
150
+ return { components, byType };
151
+ }
152
+ /**
153
+ * Analyze React hooks usage
154
+ */
155
+ async analyzeHooks() {
156
+ const files = await this.findFiles();
157
+ const hooks = [];
158
+ const customHooks = new Set();
159
+ for (const file of files) {
160
+ const source = await fs.promises.readFile(file, 'utf-8');
161
+ const result = this.extractor.extract(source, file);
162
+ const fileHooks = this.extractHooks(result.calls, result.functions, source, file);
163
+ hooks.push(...fileHooks);
164
+ // Find custom hook definitions
165
+ for (const func of result.functions) {
166
+ if (func.name.startsWith('use') && func.name.length > 3) {
167
+ customHooks.add(func.name);
168
+ }
169
+ }
170
+ }
171
+ const byType = {
172
+ builtin: hooks.filter(h => h.type === 'builtin').length,
173
+ custom: hooks.filter(h => h.type === 'custom').length,
174
+ };
175
+ return { hooks, byType, customHooks: Array.from(customHooks) };
176
+ }
177
+ /**
178
+ * Analyze error handling patterns
179
+ */
180
+ async analyzeErrorHandling() {
181
+ const files = await this.findFiles();
182
+ let tryCatchBlocks = 0;
183
+ let promiseCatches = 0;
184
+ let errorBoundaries = 0;
185
+ let throwStatements = 0;
186
+ const patterns = [];
187
+ const issues = [];
188
+ for (const file of files) {
189
+ const source = await fs.promises.readFile(file, 'utf-8');
190
+ const lines = source.split('\n');
191
+ for (let i = 0; i < lines.length; i++) {
192
+ const line = lines[i];
193
+ const lineNum = i + 1;
194
+ // Try-catch blocks
195
+ if (/\btry\s*\{/.test(line)) {
196
+ tryCatchBlocks++;
197
+ patterns.push({ type: 'try-catch', file, line: lineNum, context: line.trim() });
198
+ }
199
+ // Promise .catch()
200
+ if (/\.catch\s*\(/.test(line)) {
201
+ promiseCatches++;
202
+ patterns.push({ type: 'promise-catch', file, line: lineNum, context: line.trim() });
203
+ }
204
+ // Error boundaries (React)
205
+ if (/componentDidCatch|getDerivedStateFromError/.test(line)) {
206
+ errorBoundaries++;
207
+ patterns.push({ type: 'error-boundary', file, line: lineNum, context: line.trim() });
208
+ }
209
+ // Throw statements
210
+ if (/\bthrow\s+/.test(line)) {
211
+ throwStatements++;
212
+ patterns.push({ type: 'throw', file, line: lineNum, context: line.trim() });
213
+ }
214
+ // Empty catch blocks
215
+ if (/catch\s*\([^)]*\)\s*\{\s*\}/.test(line)) {
216
+ issues.push({
217
+ type: 'empty-catch',
218
+ file,
219
+ line: lineNum,
220
+ message: 'Empty catch block swallows errors silently',
221
+ suggestion: 'Log the error or handle it appropriately',
222
+ });
223
+ }
224
+ }
225
+ }
226
+ return {
227
+ stats: { tryCatchBlocks, promiseCatches, errorBoundaries, throwStatements },
228
+ patterns,
229
+ issues,
230
+ };
231
+ }
232
+ /**
233
+ * Analyze data access patterns
234
+ */
235
+ async analyzeDataAccess() {
236
+ const files = await this.findFiles();
237
+ const accessPoints = [];
238
+ const models = new Set();
239
+ for (const file of files) {
240
+ const source = await fs.promises.readFile(file, 'utf-8');
241
+ const fileAccessPoints = this.extractDataAccess(source, file);
242
+ accessPoints.push(...fileAccessPoints);
243
+ for (const ap of fileAccessPoints) {
244
+ if (ap.model && ap.model !== 'unknown') {
245
+ models.add(ap.model);
246
+ }
247
+ }
248
+ }
249
+ const byFramework = {};
250
+ const byOperation = {};
251
+ for (const ap of accessPoints) {
252
+ byFramework[ap.framework] = (byFramework[ap.framework] || 0) + 1;
253
+ byOperation[ap.operation] = (byOperation[ap.operation] || 0) + 1;
254
+ }
255
+ return { accessPoints, byFramework, byOperation, models: Array.from(models) };
256
+ }
257
+ /**
258
+ * Analyze decorators (NestJS, TypeORM, etc.)
259
+ */
260
+ async analyzeDecorators() {
261
+ const files = await this.findFiles();
262
+ const decorators = [];
263
+ for (const file of files) {
264
+ const source = await fs.promises.readFile(file, 'utf-8');
265
+ const fileDecorators = this.extractDecorators(source, file);
266
+ decorators.push(...fileDecorators);
267
+ }
268
+ const byName = {};
269
+ for (const dec of decorators) {
270
+ byName[dec.name] = (byName[dec.name] || 0) + 1;
271
+ }
272
+ return { decorators, byName };
273
+ }
274
+ // ============================================================================
275
+ // Private Helper Methods
276
+ // ============================================================================
277
+ async findFiles() {
278
+ const results = [];
279
+ const excludePatterns = this.config.excludePatterns ?? ['node_modules', 'dist', 'build', '.git'];
280
+ const walk = async (dir) => {
281
+ let entries;
282
+ try {
283
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
284
+ }
285
+ catch {
286
+ return;
287
+ }
288
+ for (const entry of entries) {
289
+ const fullPath = path.join(dir, entry.name);
290
+ const relativePath = path.relative(this.config.rootDir, fullPath);
291
+ const shouldExclude = excludePatterns.some((pattern) => {
292
+ if (pattern.includes('*')) {
293
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
294
+ return regex.test(relativePath);
295
+ }
296
+ return relativePath.includes(pattern);
297
+ });
298
+ if (shouldExclude)
299
+ continue;
300
+ if (entry.isDirectory()) {
301
+ await walk(fullPath);
302
+ }
303
+ else if (entry.isFile()) {
304
+ const ext = path.extname(entry.name);
305
+ if (ALL_EXTENSIONS.includes(ext)) {
306
+ results.push(fullPath);
307
+ }
308
+ }
309
+ }
310
+ };
311
+ await walk(this.config.rootDir);
312
+ return results;
313
+ }
314
+ async parsePackageJson() {
315
+ const pkgPath = path.join(this.config.rootDir, 'package.json');
316
+ try {
317
+ const content = await fs.promises.readFile(pkgPath, 'utf-8');
318
+ const pkg = JSON.parse(content);
319
+ return {
320
+ name: pkg.name ?? null,
321
+ version: pkg.version ?? null,
322
+ };
323
+ }
324
+ catch {
325
+ return { name: null, version: null };
326
+ }
327
+ }
328
+ detectFramework(importSource) {
329
+ const frameworks = {
330
+ 'react': 'react',
331
+ 'next': 'nextjs',
332
+ '@nestjs': 'nestjs',
333
+ 'express': 'express',
334
+ 'fastify': 'fastify',
335
+ 'koa': 'koa',
336
+ 'hono': 'hono',
337
+ '@prisma/client': 'prisma',
338
+ 'typeorm': 'typeorm',
339
+ 'drizzle-orm': 'drizzle',
340
+ 'sequelize': 'sequelize',
341
+ 'mongoose': 'mongoose',
342
+ '@tanstack/react-query': 'react-query',
343
+ 'redux': 'redux',
344
+ 'zustand': 'zustand',
345
+ 'vue': 'vue',
346
+ 'svelte': 'svelte',
347
+ 'angular': 'angular',
348
+ };
349
+ for (const [prefix, name] of Object.entries(frameworks)) {
350
+ if (importSource.startsWith(prefix))
351
+ return name;
352
+ }
353
+ return null;
354
+ }
355
+ isTestFile(file) {
356
+ const testPatterns = [
357
+ /\.test\.[jt]sx?$/,
358
+ /\.spec\.[jt]sx?$/,
359
+ /__tests__\//,
360
+ /\.stories\.[jt]sx?$/,
361
+ ];
362
+ return testPatterns.some((p) => p.test(file));
363
+ }
364
+ countComponents(functions, classes, source) {
365
+ let count = 0;
366
+ // Functional components: functions that return JSX
367
+ for (const func of functions) {
368
+ if (this.isReactComponent(func.name, source)) {
369
+ count++;
370
+ }
371
+ }
372
+ // Class components: classes extending React.Component
373
+ for (const cls of classes) {
374
+ if (cls.baseClasses.some((b) => b.includes('Component') || b.includes('PureComponent'))) {
375
+ count++;
376
+ }
377
+ }
378
+ return count;
379
+ }
380
+ isReactComponent(name, source) {
381
+ // Component names start with uppercase
382
+ if (!/^[A-Z]/.test(name))
383
+ return false;
384
+ // Check if function returns JSX
385
+ const funcPattern = new RegExp(`function\\s+${name}|const\\s+${name}\\s*=`);
386
+ if (!funcPattern.test(source))
387
+ return false;
388
+ // Look for JSX return
389
+ return /<[A-Z]|<[a-z]+[^>]*>/.test(source);
390
+ }
391
+ countHooks(calls) {
392
+ return calls.filter((c) => c.calleeName.startsWith('use') && c.calleeName.length > 3).length;
393
+ }
394
+ extractRoutes(source, file) {
395
+ const routes = [];
396
+ const lines = source.split('\n');
397
+ // Express patterns: app.get('/path', handler) or router.get('/path', handler)
398
+ const expressPattern = /\.(get|post|put|delete|patch|head|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
399
+ // NestJS patterns: @Get('/path'), @Post('/path'), etc.
400
+ const nestPattern = /@(Get|Post|Put|Delete|Patch|Head|Options)\s*\(\s*['"`]?([^'"`)\s]*)['"`]?\s*\)/gi;
401
+ // Next.js API routes: export async function GET/POST/etc
402
+ const nextPattern = /export\s+(?:async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)/gi;
403
+ // Fastify patterns: fastify.get('/path', handler)
404
+ const fastifyPattern = /fastify\.(get|post|put|delete|patch|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
405
+ for (let i = 0; i < lines.length; i++) {
406
+ const line = lines[i];
407
+ const lineNum = i + 1;
408
+ let match;
409
+ // Express
410
+ while ((match = expressPattern.exec(line)) !== null) {
411
+ routes.push({
412
+ method: match[1].toUpperCase(),
413
+ path: match[2],
414
+ handler: this.extractHandler(line),
415
+ framework: 'express',
416
+ file,
417
+ line: lineNum,
418
+ middleware: this.extractMiddleware(line),
419
+ decorators: [],
420
+ });
421
+ }
422
+ expressPattern.lastIndex = 0;
423
+ // NestJS
424
+ while ((match = nestPattern.exec(line)) !== null) {
425
+ routes.push({
426
+ method: match[1].toUpperCase(),
427
+ path: match[2] || '/',
428
+ handler: this.extractNestHandler(lines, i),
429
+ framework: 'nestjs',
430
+ file,
431
+ line: lineNum,
432
+ middleware: [],
433
+ decorators: this.extractNestDecorators(lines, i),
434
+ });
435
+ }
436
+ nestPattern.lastIndex = 0;
437
+ // Next.js
438
+ while ((match = nextPattern.exec(line)) !== null) {
439
+ routes.push({
440
+ method: match[1].toUpperCase(),
441
+ path: this.extractNextPath(file),
442
+ handler: match[1],
443
+ framework: 'nextjs',
444
+ file,
445
+ line: lineNum,
446
+ middleware: [],
447
+ decorators: [],
448
+ });
449
+ }
450
+ nextPattern.lastIndex = 0;
451
+ // Fastify
452
+ while ((match = fastifyPattern.exec(line)) !== null) {
453
+ routes.push({
454
+ method: match[1].toUpperCase(),
455
+ path: match[2],
456
+ handler: this.extractHandler(line),
457
+ framework: 'fastify',
458
+ file,
459
+ line: lineNum,
460
+ middleware: [],
461
+ decorators: [],
462
+ });
463
+ }
464
+ fastifyPattern.lastIndex = 0;
465
+ }
466
+ return routes;
467
+ }
468
+ extractHandler(line) {
469
+ const match = line.match(/,\s*(\w+)\s*[,)]/);
470
+ return match?.[1] ?? 'anonymous';
471
+ }
472
+ extractMiddleware(line) {
473
+ const middleware = [];
474
+ const middlewarePattern = /,\s*(\w+)\s*,/g;
475
+ let match;
476
+ while ((match = middlewarePattern.exec(line)) !== null) {
477
+ middleware.push(match[1]);
478
+ }
479
+ return middleware;
480
+ }
481
+ extractNestHandler(lines, startIndex) {
482
+ for (let i = startIndex + 1; i < Math.min(startIndex + 5, lines.length); i++) {
483
+ const match = lines[i].match(/(?:async\s+)?(\w+)\s*\(/);
484
+ if (match)
485
+ return match[1];
486
+ }
487
+ return 'unknown';
488
+ }
489
+ extractNestDecorators(lines, startIndex) {
490
+ const decorators = [];
491
+ for (let i = startIndex - 1; i >= Math.max(0, startIndex - 10); i--) {
492
+ const match = lines[i].match(/@(\w+)/);
493
+ if (match) {
494
+ decorators.push(match[1]);
495
+ }
496
+ else if (!/^\s*$/.test(lines[i])) {
497
+ break;
498
+ }
499
+ }
500
+ return decorators;
501
+ }
502
+ extractNextPath(file) {
503
+ // Convert file path to API route
504
+ // e.g., app/api/users/route.ts -> /api/users
505
+ const match = file.match(/(?:app|pages)(\/api\/[^.]+)/);
506
+ if (match) {
507
+ return match[1].replace(/\/route$/, '').replace(/\/\[([^\]]+)\]/g, '/:$1');
508
+ }
509
+ return '/';
510
+ }
511
+ extractComponents(functions, classes, source, file) {
512
+ const components = [];
513
+ // Functional components
514
+ for (const func of functions) {
515
+ if (this.isReactComponent(func.name, source)) {
516
+ components.push({
517
+ name: func.name,
518
+ type: 'functional',
519
+ file,
520
+ line: func.startLine,
521
+ props: this.extractProps(func.name, source),
522
+ hooks: this.extractHooksInFunction(func.name, source),
523
+ isExported: func.isExported,
524
+ });
525
+ }
526
+ }
527
+ // Class components
528
+ for (const cls of classes) {
529
+ if (cls.baseClasses.some((b) => b.includes('Component') || b.includes('PureComponent'))) {
530
+ components.push({
531
+ name: cls.name,
532
+ type: 'class',
533
+ file,
534
+ line: cls.startLine,
535
+ props: this.extractClassProps(cls.name, source),
536
+ hooks: [],
537
+ isExported: cls.isExported,
538
+ });
539
+ }
540
+ }
541
+ return components;
542
+ }
543
+ extractProps(funcName, source) {
544
+ const props = [];
545
+ const propsPattern = new RegExp(`${funcName}\\s*[:(]\\s*\\{([^}]+)\\}`, 'g');
546
+ const match = propsPattern.exec(source);
547
+ if (match) {
548
+ const propsStr = match[1];
549
+ const propMatches = propsStr.match(/(\w+)\s*[,:?]/g);
550
+ if (propMatches) {
551
+ for (const p of propMatches) {
552
+ props.push(p.replace(/[,:?]/g, '').trim());
553
+ }
554
+ }
555
+ }
556
+ return props;
557
+ }
558
+ extractClassProps(className, source) {
559
+ const props = [];
560
+ const propsPattern = new RegExp(`interface\\s+${className}Props\\s*\\{([^}]+)\\}`, 'g');
561
+ const match = propsPattern.exec(source);
562
+ if (match) {
563
+ const propsStr = match[1];
564
+ const propMatches = propsStr.match(/(\w+)\s*[,:?]/g);
565
+ if (propMatches) {
566
+ for (const p of propMatches) {
567
+ props.push(p.replace(/[,:?]/g, '').trim());
568
+ }
569
+ }
570
+ }
571
+ return props;
572
+ }
573
+ extractHooksInFunction(funcName, source) {
574
+ const hooks = [];
575
+ const funcPattern = new RegExp(`function\\s+${funcName}[^{]*\\{([\\s\\S]*?)\\n\\}`, 'g');
576
+ const match = funcPattern.exec(source);
577
+ if (match) {
578
+ const body = match[1];
579
+ const hookMatches = body.match(/use\w+/g);
580
+ if (hookMatches) {
581
+ hooks.push(...new Set(hookMatches));
582
+ }
583
+ }
584
+ return hooks;
585
+ }
586
+ extractHooks(calls, _functions, _source, file) {
587
+ const hooks = [];
588
+ const builtinHooks = [
589
+ 'useState', 'useEffect', 'useContext', 'useReducer', 'useCallback',
590
+ 'useMemo', 'useRef', 'useImperativeHandle', 'useLayoutEffect',
591
+ 'useDebugValue', 'useDeferredValue', 'useTransition', 'useId',
592
+ 'useSyncExternalStore', 'useInsertionEffect',
593
+ ];
594
+ for (const call of calls) {
595
+ if (call.calleeName.startsWith('use') && call.calleeName.length > 3) {
596
+ const isBuiltin = builtinHooks.includes(call.calleeName);
597
+ hooks.push({
598
+ name: call.calleeName,
599
+ type: isBuiltin ? 'builtin' : 'custom',
600
+ file,
601
+ line: call.line,
602
+ dependencies: [],
603
+ });
604
+ }
605
+ }
606
+ return hooks;
607
+ }
608
+ extractDataAccess(source, file) {
609
+ const accessPoints = [];
610
+ const lines = source.split('\n');
611
+ // Prisma patterns: prisma.user.findMany(), prisma.$queryRaw
612
+ const prismaPattern = /prisma\.(\w+)\.(findMany|findFirst|findUnique|create|update|delete|upsert|count|aggregate)/gi;
613
+ const prismaRawPattern = /prisma\.\$queryRaw/gi;
614
+ // TypeORM patterns: repository.find(), getRepository(User).find()
615
+ const typeormPattern = /(?:repository|getRepository\(\w+\))\.(find|findOne|save|remove|delete|update|insert)/gi;
616
+ // Drizzle patterns: db.select().from(users)
617
+ const drizzlePattern = /db\.(select|insert|update|delete)\(\)/gi;
618
+ // Sequelize patterns: User.findAll(), Model.create()
619
+ const sequelizePattern = /(\w+)\.(findAll|findOne|findByPk|create|update|destroy|bulkCreate)/gi;
620
+ // Mongoose patterns: Model.find(), Model.findById()
621
+ const mongoosePattern = /(\w+)\.(find|findOne|findById|create|updateOne|deleteOne|aggregate)/gi;
622
+ for (let i = 0; i < lines.length; i++) {
623
+ const line = lines[i];
624
+ const lineNum = i + 1;
625
+ let match;
626
+ // Prisma
627
+ while ((match = prismaPattern.exec(line)) !== null) {
628
+ accessPoints.push({
629
+ model: match[1],
630
+ operation: this.normalizeOperation(match[2]),
631
+ framework: 'prisma',
632
+ file,
633
+ line: lineNum,
634
+ isRawSql: false,
635
+ });
636
+ }
637
+ prismaPattern.lastIndex = 0;
638
+ // Prisma raw
639
+ if (prismaRawPattern.test(line)) {
640
+ accessPoints.push({
641
+ model: 'raw',
642
+ operation: 'query',
643
+ framework: 'prisma',
644
+ file,
645
+ line: lineNum,
646
+ isRawSql: true,
647
+ });
648
+ }
649
+ prismaRawPattern.lastIndex = 0;
650
+ // TypeORM
651
+ while ((match = typeormPattern.exec(line)) !== null) {
652
+ accessPoints.push({
653
+ model: 'unknown',
654
+ operation: this.normalizeOperation(match[1]),
655
+ framework: 'typeorm',
656
+ file,
657
+ line: lineNum,
658
+ isRawSql: false,
659
+ });
660
+ }
661
+ typeormPattern.lastIndex = 0;
662
+ // Drizzle
663
+ while ((match = drizzlePattern.exec(line)) !== null) {
664
+ accessPoints.push({
665
+ model: 'unknown',
666
+ operation: this.normalizeOperation(match[1]),
667
+ framework: 'drizzle',
668
+ file,
669
+ line: lineNum,
670
+ isRawSql: false,
671
+ });
672
+ }
673
+ drizzlePattern.lastIndex = 0;
674
+ // Sequelize
675
+ while ((match = sequelizePattern.exec(line)) !== null) {
676
+ accessPoints.push({
677
+ model: match[1],
678
+ operation: this.normalizeOperation(match[2]),
679
+ framework: 'sequelize',
680
+ file,
681
+ line: lineNum,
682
+ isRawSql: false,
683
+ });
684
+ }
685
+ sequelizePattern.lastIndex = 0;
686
+ // Mongoose
687
+ while ((match = mongoosePattern.exec(line)) !== null) {
688
+ accessPoints.push({
689
+ model: match[1],
690
+ operation: this.normalizeOperation(match[2]),
691
+ framework: 'mongoose',
692
+ file,
693
+ line: lineNum,
694
+ isRawSql: false,
695
+ });
696
+ }
697
+ mongoosePattern.lastIndex = 0;
698
+ }
699
+ return accessPoints;
700
+ }
701
+ normalizeOperation(op) {
702
+ const readOps = ['find', 'findMany', 'findFirst', 'findUnique', 'findOne', 'findAll', 'findById', 'findByPk', 'select', 'count', 'aggregate'];
703
+ const writeOps = ['create', 'insert', 'save', 'bulkCreate', 'upsert'];
704
+ const updateOps = ['update', 'updateOne', 'updateMany'];
705
+ const deleteOps = ['delete', 'remove', 'destroy', 'deleteOne', 'deleteMany'];
706
+ const opLower = op.toLowerCase();
707
+ if (readOps.some((r) => opLower.includes(r.toLowerCase())))
708
+ return 'read';
709
+ if (writeOps.some((w) => opLower.includes(w.toLowerCase())))
710
+ return 'write';
711
+ if (updateOps.some((u) => opLower.includes(u.toLowerCase())))
712
+ return 'update';
713
+ if (deleteOps.some((d) => opLower.includes(d.toLowerCase())))
714
+ return 'delete';
715
+ return 'unknown';
716
+ }
717
+ extractDecorators(source, file) {
718
+ const decorators = [];
719
+ const lines = source.split('\n');
720
+ const decoratorPattern = /@(\w+)\s*\(([^)]*)\)?/g;
721
+ for (let i = 0; i < lines.length; i++) {
722
+ const line = lines[i];
723
+ const lineNum = i + 1;
724
+ let match;
725
+ while ((match = decoratorPattern.exec(line)) !== null) {
726
+ const target = this.determineDecoratorTarget(lines, i);
727
+ decorators.push({
728
+ name: match[1],
729
+ target,
730
+ file,
731
+ line: lineNum,
732
+ arguments: match[2] ? match[2].split(',').map((a) => a.trim()) : [],
733
+ });
734
+ }
735
+ decoratorPattern.lastIndex = 0;
736
+ }
737
+ return decorators;
738
+ }
739
+ determineDecoratorTarget(lines, decoratorLine) {
740
+ // Look at the next non-decorator line
741
+ for (let i = decoratorLine + 1; i < Math.min(decoratorLine + 10, lines.length); i++) {
742
+ const line = lines[i].trim();
743
+ if (line.startsWith('@'))
744
+ continue;
745
+ if (/^(export\s+)?(abstract\s+)?class\s/.test(line))
746
+ return 'class';
747
+ if (/^(async\s+)?(\w+)\s*\(/.test(line))
748
+ return 'method';
749
+ if (/^\w+\s*[?:]/.test(line))
750
+ return 'property';
751
+ break;
752
+ }
753
+ return 'method';
754
+ }
755
+ }
756
+ /**
757
+ * Factory function
758
+ */
759
+ export function createTypeScriptAnalyzer(config) {
760
+ return new TypeScriptAnalyzer(config);
761
+ }
762
+ //# sourceMappingURL=typescript-analyzer.js.map