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.
- package/dist/call-graph/store/call-graph-store.d.ts +13 -1
- package/dist/call-graph/store/call-graph-store.d.ts.map +1 -1
- package/dist/call-graph/store/call-graph-store.js +164 -1
- package/dist/call-graph/store/call-graph-store.js.map +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -1
- package/dist/java/index.d.ts +8 -0
- package/dist/java/index.d.ts.map +1 -0
- package/dist/java/index.js +7 -0
- package/dist/java/index.js.map +1 -0
- package/dist/java/java-analyzer.d.ts +142 -0
- package/dist/java/java-analyzer.d.ts.map +1 -0
- package/dist/java/java-analyzer.js +515 -0
- package/dist/java/java-analyzer.js.map +1 -0
- package/dist/php/index.d.ts +8 -0
- package/dist/php/index.d.ts.map +1 -0
- package/dist/php/index.js +7 -0
- package/dist/php/index.js.map +1 -0
- package/dist/php/php-analyzer.d.ts +149 -0
- package/dist/php/php-analyzer.d.ts.map +1 -0
- package/dist/php/php-analyzer.js +546 -0
- package/dist/php/php-analyzer.js.map +1 -0
- package/dist/python/index.d.ts +8 -0
- package/dist/python/index.d.ts.map +1 -0
- package/dist/python/index.js +7 -0
- package/dist/python/index.js.map +1 -0
- package/dist/python/python-analyzer.d.ts +156 -0
- package/dist/python/python-analyzer.d.ts.map +1 -0
- package/dist/python/python-analyzer.js +535 -0
- package/dist/python/python-analyzer.js.map +1 -0
- package/dist/typescript/index.d.ts +13 -0
- package/dist/typescript/index.d.ts.map +1 -0
- package/dist/typescript/index.js +12 -0
- package/dist/typescript/index.js.map +1 -0
- package/dist/typescript/typescript-analyzer.d.ts +194 -0
- package/dist/typescript/typescript-analyzer.d.ts.map +1 -0
- package/dist/typescript/typescript-analyzer.js +762 -0
- package/dist/typescript/typescript-analyzer.js.map +1 -0
- 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
|