codedev-mcp 3.2.2 → 3.2.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/analyzers/api-contract.d.ts +1 -1
- package/dist/analyzers/api-contract.d.ts.map +1 -1
- package/dist/analyzers/api-contract.js +839 -13
- package/dist/analyzers/api-contract.js.map +1 -1
- package/dist/analyzers/db-schema.d.ts.map +1 -1
- package/dist/analyzers/db-schema.js +88 -30
- package/dist/analyzers/db-schema.js.map +1 -1
- package/dist/analyzers/dep-vuln.d.ts +1 -0
- package/dist/analyzers/dep-vuln.d.ts.map +1 -1
- package/dist/analyzers/dep-vuln.js +163 -25
- package/dist/analyzers/dep-vuln.js.map +1 -1
- package/dist/db/sqlite-store.d.ts +7 -0
- package/dist/db/sqlite-store.d.ts.map +1 -1
- package/dist/db/sqlite-store.js +34 -13
- package/dist/db/sqlite-store.js.map +1 -1
- package/dist/tools/quality.d.ts.map +1 -1
- package/dist/tools/quality.js +392 -80
- package/dist/tools/quality.js.map +1 -1
- package/dist/tools/security.d.ts.map +1 -1
- package/dist/tools/security.js +9 -2
- package/dist/tools/security.js.map +1 -1
- package/package.json +1 -1
|
@@ -100,24 +100,113 @@ function parseGraphQL(content, file) {
|
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
102
|
* Parse Express/Fastify route definitions.
|
|
103
|
+
* Supports multiple Express patterns:
|
|
104
|
+
* - router.get('/path', handler)
|
|
105
|
+
* - app.post('/path', handler)
|
|
106
|
+
* - router.route('/path').get(handler).post(handler)
|
|
107
|
+
* - express.Router().get('/path', handler)
|
|
108
|
+
* - Routes with variables: router.get(pathVar, handler)
|
|
109
|
+
* - Routes with template literals: router.get(`/api/${version}/users`, handler)
|
|
103
110
|
* @param content - The file content to parse.
|
|
104
111
|
* @param file - The file path.
|
|
105
112
|
* @returns Parsed API endpoints.
|
|
106
113
|
*/
|
|
107
114
|
function parseExpressRoutes(content, file) {
|
|
108
115
|
const endpoints = [];
|
|
109
|
-
|
|
116
|
+
// Pattern 1: Standard router.get/post/put/delete/patch('/path', ...)
|
|
117
|
+
// Matches: router.get('/api/users', handler) or app.post('/api/users', handler)
|
|
118
|
+
const standardRouteRegex = /(?:app|router|express\.Router\(\)|express\(\))\.(get|post|put|delete|patch|all|use)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
110
119
|
let match;
|
|
111
|
-
while ((match =
|
|
120
|
+
while ((match = standardRouteRegex.exec(content)) !== null) {
|
|
121
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
122
|
+
const method = match[1].toUpperCase();
|
|
123
|
+
const path = match[2];
|
|
124
|
+
// Skip 'use' and 'all' methods unless they have specific paths
|
|
125
|
+
if (method === 'USE' && !path.match(/^\/[^/]/))
|
|
126
|
+
continue;
|
|
127
|
+
endpoints.push({
|
|
128
|
+
method: method === 'ALL' ? 'ANY' : method,
|
|
129
|
+
path: path,
|
|
130
|
+
file,
|
|
131
|
+
line,
|
|
132
|
+
source: 'express',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Pattern 2: router.route('/path').get(...).post(...)
|
|
136
|
+
const routeChainRegex = /(?:app|router)\.route\s*\(\s*['"`]([^'"`]+)['"`]\s*\)\s*\.(get|post|put|delete|patch)\s*\(/gi;
|
|
137
|
+
while ((match = routeChainRegex.exec(content)) !== null) {
|
|
112
138
|
const line = content.substring(0, match.index).split('\n').length;
|
|
139
|
+
endpoints.push({
|
|
140
|
+
method: match[2].toUpperCase(),
|
|
141
|
+
path: match[1],
|
|
142
|
+
file,
|
|
143
|
+
line,
|
|
144
|
+
source: 'express',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Pattern 3: Routes with variables (router.get(pathVar, handler))
|
|
148
|
+
// Try to find path variables defined earlier in the file
|
|
149
|
+
const pathVarRegex = /(?:const|let|var)\s+(\w+Path)\s*=\s*['"`]([^'"`]+)['"`]/g;
|
|
150
|
+
const pathVars = new Map();
|
|
151
|
+
let pathMatch;
|
|
152
|
+
while ((pathMatch = pathVarRegex.exec(content)) !== null) {
|
|
153
|
+
pathVars.set(pathMatch[1], pathMatch[2]);
|
|
154
|
+
}
|
|
155
|
+
// Pattern 4: Routes using path variables
|
|
156
|
+
const varRouteRegex = /(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*(\w+Path)/gi;
|
|
157
|
+
while ((match = varRouteRegex.exec(content)) !== null) {
|
|
158
|
+
const pathVar = match[2];
|
|
159
|
+
const pathValue = pathVars.get(pathVar);
|
|
160
|
+
if (pathValue) {
|
|
161
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
162
|
+
endpoints.push({
|
|
163
|
+
method: match[1].toUpperCase(),
|
|
164
|
+
path: pathValue,
|
|
165
|
+
file,
|
|
166
|
+
line,
|
|
167
|
+
source: 'express',
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Pattern 5: Template literal routes: router.get(`/api/${version}/users`, ...)
|
|
172
|
+
const templateRouteRegex = /(?:app|router)\.(get|post|put|delete|patch)\s*\(\s*`([^`]+)`/gi;
|
|
173
|
+
while ((match = templateRouteRegex.exec(content)) !== null) {
|
|
174
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
175
|
+
// Extract static parts of template literal (remove ${...} parts)
|
|
176
|
+
const path = match[2].replace(/\$\{[^}]+\}/g, '*');
|
|
113
177
|
endpoints.push({
|
|
114
178
|
method: match[1].toUpperCase(),
|
|
115
|
-
path:
|
|
179
|
+
path: path,
|
|
116
180
|
file,
|
|
117
181
|
line,
|
|
118
182
|
source: 'express',
|
|
119
183
|
});
|
|
120
184
|
}
|
|
185
|
+
// Pattern 6: Express Router instances: const router = express.Router(); router.get(...)
|
|
186
|
+
// This is already covered by Pattern 1, but let's also check for mounted routers
|
|
187
|
+
const mountedRouterRegex = /(?:app|router)\.use\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*(\w+Router|\w+Routes)/gi;
|
|
188
|
+
while ((match = mountedRouterRegex.exec(content)) !== null) {
|
|
189
|
+
const basePath = match[1];
|
|
190
|
+
const routerName = match[2];
|
|
191
|
+
// Try to find routes in the router definition
|
|
192
|
+
const routerDefRegex = new RegExp(`(?:const|let|var)\\s+${routerName}\\s*=\\s*express\\.Router\\(\\)[\\s\\S]*?`, 'i');
|
|
193
|
+
const routerDef = content.match(routerDefRegex);
|
|
194
|
+
if (routerDef) {
|
|
195
|
+
const routerContent = routerDef[0];
|
|
196
|
+
const routerRouteRegex = new RegExp(`(?:router|${routerName})\\.(get|post|put|delete|patch)\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`, 'gi');
|
|
197
|
+
const routerRoutes = routerContent.matchAll(routerRouteRegex);
|
|
198
|
+
for (const routeMatch of routerRoutes) {
|
|
199
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
200
|
+
endpoints.push({
|
|
201
|
+
method: routeMatch[1].toUpperCase(),
|
|
202
|
+
path: `${basePath}${routeMatch[2]}`.replace(/\/+/g, '/'),
|
|
203
|
+
file,
|
|
204
|
+
line,
|
|
205
|
+
source: 'express',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
121
210
|
return endpoints;
|
|
122
211
|
}
|
|
123
212
|
/**
|
|
@@ -180,6 +269,503 @@ function parseNestJSRoutes(content, file) {
|
|
|
180
269
|
}
|
|
181
270
|
return endpoints;
|
|
182
271
|
}
|
|
272
|
+
/**
|
|
273
|
+
* Parse Flask route decorators.
|
|
274
|
+
* @param content - The file content to parse.
|
|
275
|
+
* @param file - The file path.
|
|
276
|
+
* @returns Parsed API endpoints.
|
|
277
|
+
*/
|
|
278
|
+
function parseFlaskRoutes(content, file) {
|
|
279
|
+
const endpoints = [];
|
|
280
|
+
const routeRegex = /@(?:app|blueprint|router)\.(route|get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
281
|
+
let match;
|
|
282
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
283
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
284
|
+
const method = match[1].toUpperCase() === 'ROUTE' ? 'GET' : match[1].toUpperCase();
|
|
285
|
+
const path = match[2];
|
|
286
|
+
endpoints.push({
|
|
287
|
+
method,
|
|
288
|
+
path,
|
|
289
|
+
file,
|
|
290
|
+
line,
|
|
291
|
+
source: 'flask',
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return endpoints;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Parse Django REST Framework viewsets and views.
|
|
298
|
+
* @param content - The file content to parse.
|
|
299
|
+
* @param file - The file path.
|
|
300
|
+
* @returns Parsed API endpoints.
|
|
301
|
+
*/
|
|
302
|
+
function parseDjangoRoutes(content, file) {
|
|
303
|
+
const endpoints = [];
|
|
304
|
+
// Django REST Framework ViewSet with router
|
|
305
|
+
if (/class\s+\w+ViewSet/.test(content) || /from\s+rest_framework/.test(content)) {
|
|
306
|
+
const viewsetMatch = content.match(/class\s+(\w+ViewSet)/);
|
|
307
|
+
if (viewsetMatch) {
|
|
308
|
+
// Common ViewSet actions
|
|
309
|
+
const actions = ['list', 'create', 'retrieve', 'update', 'partial_update', 'destroy'];
|
|
310
|
+
for (const action of actions) {
|
|
311
|
+
if (new RegExp(`def\\s+${action}`).test(content)) {
|
|
312
|
+
endpoints.push({
|
|
313
|
+
method: action === 'list' || action === 'retrieve'
|
|
314
|
+
? 'GET'
|
|
315
|
+
: action === 'create'
|
|
316
|
+
? 'POST'
|
|
317
|
+
: action === 'destroy'
|
|
318
|
+
? 'DELETE'
|
|
319
|
+
: 'PUT',
|
|
320
|
+
path: `/${action}`,
|
|
321
|
+
file,
|
|
322
|
+
line: 0,
|
|
323
|
+
source: 'django',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Django function-based views with decorators
|
|
330
|
+
const decoratorRegex = /@(?:api_view|action)\s*\([^)]*\)\s*(?:@\w+\s*\([^)]*\)\s*)*def\s+(\w+)\s*\(/gi;
|
|
331
|
+
let match;
|
|
332
|
+
while ((match = decoratorRegex.exec(content)) !== null) {
|
|
333
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
334
|
+
endpoints.push({
|
|
335
|
+
method: 'GET',
|
|
336
|
+
path: `/${match[1]}`,
|
|
337
|
+
file,
|
|
338
|
+
line,
|
|
339
|
+
source: 'django',
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return endpoints;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Parse Rails routes.rb file.
|
|
346
|
+
* @param content - The file content to parse.
|
|
347
|
+
* @param file - The file path.
|
|
348
|
+
* @returns Parsed API endpoints.
|
|
349
|
+
*/
|
|
350
|
+
function parseRailsRoutes(content, file) {
|
|
351
|
+
const endpoints = [];
|
|
352
|
+
// Rails route syntax: get '/users', to: 'users#index'
|
|
353
|
+
const routeRegex = /(get|post|put|patch|delete|resources?)\s+['"]([^'"]+)['"]/gi;
|
|
354
|
+
let match;
|
|
355
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
356
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
357
|
+
const method = match[1].toUpperCase();
|
|
358
|
+
const path = match[2];
|
|
359
|
+
if (method === 'RESOURCES' || method === 'RESOURCE') {
|
|
360
|
+
// RESTful resource routes
|
|
361
|
+
const resourceName = path.replace(/^\//, '').replace(/\/$/, '');
|
|
362
|
+
endpoints.push({ method: 'GET', path: `/${resourceName}`, file, line, source: 'rails' }, { method: 'POST', path: `/${resourceName}`, file, line, source: 'rails' }, { method: 'GET', path: `/${resourceName}/:id`, file, line, source: 'rails' }, { method: 'PUT', path: `/${resourceName}/:id`, file, line, source: 'rails' }, { method: 'DELETE', path: `/${resourceName}/:id`, file, line, source: 'rails' });
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
endpoints.push({
|
|
366
|
+
method: method === 'PATCH' ? 'PUT' : method,
|
|
367
|
+
path,
|
|
368
|
+
file,
|
|
369
|
+
line,
|
|
370
|
+
source: 'rails',
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return endpoints;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Parse Sinatra routes.
|
|
378
|
+
* @param content - The file content to parse.
|
|
379
|
+
* @param file - The file path.
|
|
380
|
+
* @returns Parsed API endpoints.
|
|
381
|
+
*/
|
|
382
|
+
function parseSinatraRoutes(content, file) {
|
|
383
|
+
const endpoints = [];
|
|
384
|
+
const routeRegex = /(get|post|put|delete|patch)\s+['"]([^'"]+)['"]/gi;
|
|
385
|
+
let match;
|
|
386
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
387
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
388
|
+
endpoints.push({
|
|
389
|
+
method: match[1].toUpperCase(),
|
|
390
|
+
path: match[2],
|
|
391
|
+
file,
|
|
392
|
+
line,
|
|
393
|
+
source: 'sinatra',
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return endpoints;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Parse Laravel routes.
|
|
400
|
+
* @param content - The file content to parse.
|
|
401
|
+
* @param file - The file path.
|
|
402
|
+
* @returns Parsed API endpoints.
|
|
403
|
+
*/
|
|
404
|
+
function parseLaravelRoutes(content, file) {
|
|
405
|
+
const endpoints = [];
|
|
406
|
+
// Laravel route syntax: Route::get('/users', [UserController::class, 'index']);
|
|
407
|
+
const routeRegex = /Route::(get|post|put|delete|patch|any|match)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
408
|
+
let match;
|
|
409
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
410
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
411
|
+
const method = match[1].toUpperCase();
|
|
412
|
+
endpoints.push({
|
|
413
|
+
method: method === 'ANY' || method === 'MATCH' ? 'ANY' : method,
|
|
414
|
+
path: match[2],
|
|
415
|
+
file,
|
|
416
|
+
line,
|
|
417
|
+
source: 'laravel',
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
// Laravel resource routes: Route::resource('users', UserController::class);
|
|
421
|
+
const resourceRegex = /Route::resource\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
422
|
+
while ((match = resourceRegex.exec(content)) !== null) {
|
|
423
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
424
|
+
const resourceName = match[1];
|
|
425
|
+
endpoints.push({ method: 'GET', path: `/${resourceName}`, file, line, source: 'laravel' }, { method: 'POST', path: `/${resourceName}`, file, line, source: 'laravel' }, { method: 'GET', path: `/${resourceName}/{id}`, file, line, source: 'laravel' }, { method: 'PUT', path: `/${resourceName}/{id}`, file, line, source: 'laravel' }, { method: 'DELETE', path: `/${resourceName}/{id}`, file, line, source: 'laravel' });
|
|
426
|
+
}
|
|
427
|
+
return endpoints;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Parse Spring Boot @RequestMapping annotations.
|
|
431
|
+
* @param content - The file content to parse.
|
|
432
|
+
* @param file - The file path.
|
|
433
|
+
* @returns Parsed API endpoints.
|
|
434
|
+
*/
|
|
435
|
+
function parseSpringRoutes(content, file) {
|
|
436
|
+
const endpoints = [];
|
|
437
|
+
if (!/@(?:RestController|Controller)/.test(content))
|
|
438
|
+
return endpoints;
|
|
439
|
+
const classPath = content.match(/@(?:RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\s*\(\s*value\s*=\s*['"]([^'"]+)['"]/)?.[1] ||
|
|
440
|
+
content.match(/@RequestMapping\s*\(\s*['"]([^'"]+)['"]/)?.[1] ||
|
|
441
|
+
'';
|
|
442
|
+
const methodRegex = /@(?:GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping|RequestMapping)\s*\(\s*(?:value\s*=\s*)?['"]([^'"]*)['"]/gi;
|
|
443
|
+
let match;
|
|
444
|
+
while ((match = methodRegex.exec(content)) !== null) {
|
|
445
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
446
|
+
const methodPath = match[1];
|
|
447
|
+
const fullAnnotation = content.substring(match.index, content.indexOf(')', match.index) + 1);
|
|
448
|
+
let method = 'GET';
|
|
449
|
+
if (/GetMapping/.test(fullAnnotation))
|
|
450
|
+
method = 'GET';
|
|
451
|
+
else if (/PostMapping/.test(fullAnnotation))
|
|
452
|
+
method = 'POST';
|
|
453
|
+
else if (/PutMapping/.test(fullAnnotation))
|
|
454
|
+
method = 'PUT';
|
|
455
|
+
else if (/DeleteMapping/.test(fullAnnotation))
|
|
456
|
+
method = 'DELETE';
|
|
457
|
+
else if (/PatchMapping/.test(fullAnnotation))
|
|
458
|
+
method = 'PATCH';
|
|
459
|
+
else if (/RequestMapping/.test(fullAnnotation)) {
|
|
460
|
+
const methodMatch = fullAnnotation.match(/method\s*=\s*RequestMethod\.(\w+)/);
|
|
461
|
+
if (methodMatch)
|
|
462
|
+
method = methodMatch[1].toUpperCase();
|
|
463
|
+
}
|
|
464
|
+
endpoints.push({
|
|
465
|
+
method,
|
|
466
|
+
path: `/${classPath}/${methodPath}`.replace(/\/+/g, '/'),
|
|
467
|
+
file,
|
|
468
|
+
line,
|
|
469
|
+
source: 'spring',
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return endpoints;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Parse JAX-RS annotations.
|
|
476
|
+
* @param content - The file content to parse.
|
|
477
|
+
* @param file - The file path.
|
|
478
|
+
* @returns Parsed API endpoints.
|
|
479
|
+
*/
|
|
480
|
+
function parseJAXRSRoutes(content, file) {
|
|
481
|
+
const endpoints = [];
|
|
482
|
+
if (!/@Path/.test(content))
|
|
483
|
+
return endpoints;
|
|
484
|
+
const classPath = content.match(/@Path\s*\(\s*['"]([^'"]+)['"]/)?.[1] || '';
|
|
485
|
+
const methodRegex = /@(GET|POST|PUT|DELETE|PATCH|Path)\s*\(\s*(?:value\s*=\s*)?['"]([^'"]*)['"]/gi;
|
|
486
|
+
let match;
|
|
487
|
+
while ((match = methodRegex.exec(content)) !== null) {
|
|
488
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
489
|
+
const annotation = match[1];
|
|
490
|
+
const path = match[2] || '';
|
|
491
|
+
let method = 'GET';
|
|
492
|
+
if (annotation === 'GET')
|
|
493
|
+
method = 'GET';
|
|
494
|
+
else if (annotation === 'POST')
|
|
495
|
+
method = 'POST';
|
|
496
|
+
else if (annotation === 'PUT')
|
|
497
|
+
method = 'PUT';
|
|
498
|
+
else if (annotation === 'DELETE')
|
|
499
|
+
method = 'DELETE';
|
|
500
|
+
else if (annotation === 'PATCH')
|
|
501
|
+
method = 'PATCH';
|
|
502
|
+
endpoints.push({
|
|
503
|
+
method,
|
|
504
|
+
path: `/${classPath}/${path}`.replace(/\/+/g, '/'),
|
|
505
|
+
file,
|
|
506
|
+
line,
|
|
507
|
+
source: 'jaxrs',
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return endpoints;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Parse Go Gin routes.
|
|
514
|
+
* @param content - The file content to parse.
|
|
515
|
+
* @param file - The file path.
|
|
516
|
+
* @returns Parsed API endpoints.
|
|
517
|
+
*/
|
|
518
|
+
function parseGinRoutes(content, file) {
|
|
519
|
+
const endpoints = [];
|
|
520
|
+
const routeRegex = /(?:router|r|engine)\.(GET|POST|PUT|DELETE|PATCH|Any)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
521
|
+
let match;
|
|
522
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
523
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
524
|
+
endpoints.push({
|
|
525
|
+
method: match[1] === 'Any' ? 'ANY' : match[1],
|
|
526
|
+
path: match[2],
|
|
527
|
+
file,
|
|
528
|
+
line,
|
|
529
|
+
source: 'gin',
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return endpoints;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Parse Go Echo routes.
|
|
536
|
+
* @param content - The file content to parse.
|
|
537
|
+
* @param file - The file path.
|
|
538
|
+
* @returns Parsed API endpoints.
|
|
539
|
+
*/
|
|
540
|
+
function parseEchoRoutes(content, file) {
|
|
541
|
+
const endpoints = [];
|
|
542
|
+
const routeRegex = /(?:e|app|router)\.(GET|POST|PUT|DELETE|PATCH|Any)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
543
|
+
let match;
|
|
544
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
545
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
546
|
+
endpoints.push({
|
|
547
|
+
method: match[1] === 'Any' ? 'ANY' : match[1],
|
|
548
|
+
path: match[2],
|
|
549
|
+
file,
|
|
550
|
+
line,
|
|
551
|
+
source: 'echo',
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return endpoints;
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Parse Go Fiber routes.
|
|
558
|
+
* @param content - The file content to parse.
|
|
559
|
+
* @param file - The file path.
|
|
560
|
+
* @returns Parsed API endpoints.
|
|
561
|
+
*/
|
|
562
|
+
function parseFiberRoutes(content, file) {
|
|
563
|
+
const endpoints = [];
|
|
564
|
+
const routeRegex = /(?:app|router)\.(Get|Post|Put|Delete|Patch|All)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
565
|
+
let match;
|
|
566
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
567
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
568
|
+
endpoints.push({
|
|
569
|
+
method: match[1] === 'All' ? 'ANY' : match[1].toUpperCase(),
|
|
570
|
+
path: match[2],
|
|
571
|
+
file,
|
|
572
|
+
line,
|
|
573
|
+
source: 'fiber',
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
return endpoints;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Parse Go Chi routes.
|
|
580
|
+
* @param content - The file content to parse.
|
|
581
|
+
* @param file - The file path.
|
|
582
|
+
* @returns Parsed API endpoints.
|
|
583
|
+
*/
|
|
584
|
+
function parseChiRoutes(content, file) {
|
|
585
|
+
const endpoints = [];
|
|
586
|
+
const routeRegex = /(?:r|router|mux)\.(Get|Post|Put|Delete|Patch|Method)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
587
|
+
let match;
|
|
588
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
589
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
590
|
+
endpoints.push({
|
|
591
|
+
method: match[1] === 'Method' ? 'ANY' : match[1].toUpperCase(),
|
|
592
|
+
path: match[2],
|
|
593
|
+
file,
|
|
594
|
+
line,
|
|
595
|
+
source: 'chi',
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return endpoints;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Parse Rust Actix-web routes.
|
|
602
|
+
* @param content - The file content to parse.
|
|
603
|
+
* @param file - The file path.
|
|
604
|
+
* @returns Parsed API endpoints.
|
|
605
|
+
*/
|
|
606
|
+
function parseActixRoutes(content, file) {
|
|
607
|
+
const endpoints = [];
|
|
608
|
+
const routeRegex = /\.(route|get|post|put|delete)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
609
|
+
let match;
|
|
610
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
611
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
612
|
+
const method = match[1].toUpperCase() === 'ROUTE' ? 'GET' : match[1].toUpperCase();
|
|
613
|
+
endpoints.push({
|
|
614
|
+
method,
|
|
615
|
+
path: match[2],
|
|
616
|
+
file,
|
|
617
|
+
line,
|
|
618
|
+
source: 'actix',
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
return endpoints;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Parse Rust Rocket routes.
|
|
625
|
+
* @param content - The file content to parse.
|
|
626
|
+
* @param file - The file path.
|
|
627
|
+
* @returns Parsed API endpoints.
|
|
628
|
+
*/
|
|
629
|
+
function parseRocketRoutes(content, file) {
|
|
630
|
+
const endpoints = [];
|
|
631
|
+
const routeRegex = /#\[(get|post|put|delete|patch|head|options)\s*\(['"]([^'"]+)['"]/gi;
|
|
632
|
+
let match;
|
|
633
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
634
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
635
|
+
endpoints.push({
|
|
636
|
+
method: match[1].toUpperCase(),
|
|
637
|
+
path: match[2],
|
|
638
|
+
file,
|
|
639
|
+
line,
|
|
640
|
+
source: 'rocket',
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return endpoints;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Parse Rust Axum routes.
|
|
647
|
+
* @param content - The file content to parse.
|
|
648
|
+
* @param file - The file path.
|
|
649
|
+
* @returns Parsed API endpoints.
|
|
650
|
+
*/
|
|
651
|
+
function parseAxumRoutes(content, file) {
|
|
652
|
+
const endpoints = [];
|
|
653
|
+
const routeRegex = /\.(route|get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
654
|
+
let match;
|
|
655
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
656
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
657
|
+
const method = match[1].toUpperCase() === 'ROUTE' ? 'GET' : match[1].toUpperCase();
|
|
658
|
+
endpoints.push({
|
|
659
|
+
method,
|
|
660
|
+
path: match[2],
|
|
661
|
+
file,
|
|
662
|
+
line,
|
|
663
|
+
source: 'axum',
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
return endpoints;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Parse ASP.NET Core controllers.
|
|
670
|
+
* @param content - The file content to parse.
|
|
671
|
+
* @param file - The file path.
|
|
672
|
+
* @returns Parsed API endpoints.
|
|
673
|
+
*/
|
|
674
|
+
function parseAspNetRoutes(content, file) {
|
|
675
|
+
const endpoints = [];
|
|
676
|
+
if (!/\[ApiController\]/.test(content) && !/class\s+\w+Controller/.test(content))
|
|
677
|
+
return endpoints;
|
|
678
|
+
const routePrefix = content.match(/\[Route\s*\(\s*['"]([^'"]+)['"]/)?.[1] || '';
|
|
679
|
+
const methodRegex = /\[(HttpGet|HttpPost|HttpPut|HttpDelete|HttpPatch)\s*(?:\(\s*['"]([^'"]*)['"]\s*)?\)\]/gi;
|
|
680
|
+
let match;
|
|
681
|
+
while ((match = methodRegex.exec(content)) !== null) {
|
|
682
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
683
|
+
const method = match[1].replace('Http', '').toUpperCase();
|
|
684
|
+
const path = match[2] || '';
|
|
685
|
+
endpoints.push({
|
|
686
|
+
method,
|
|
687
|
+
path: `/${routePrefix}/${path}`.replace(/\/+/g, '/'),
|
|
688
|
+
file,
|
|
689
|
+
line,
|
|
690
|
+
source: 'aspnet',
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return endpoints;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Parse Koa routes.
|
|
697
|
+
* @param content - The file content to parse.
|
|
698
|
+
* @param file - The file path.
|
|
699
|
+
* @returns Parsed API endpoints.
|
|
700
|
+
*/
|
|
701
|
+
function parseKoaRoutes(content, file) {
|
|
702
|
+
const endpoints = [];
|
|
703
|
+
const routeRegex = /(?:router|app)\.(get|post|put|delete|patch|all)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
704
|
+
let match;
|
|
705
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
706
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
707
|
+
endpoints.push({
|
|
708
|
+
method: match[1].toUpperCase() === 'ALL' ? 'ANY' : match[1].toUpperCase(),
|
|
709
|
+
path: match[2],
|
|
710
|
+
file,
|
|
711
|
+
line,
|
|
712
|
+
source: 'koa',
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return endpoints;
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Parse Fastify routes.
|
|
719
|
+
* @param content - The file content to parse.
|
|
720
|
+
* @param file - The file path.
|
|
721
|
+
* @returns Parsed API endpoints.
|
|
722
|
+
*/
|
|
723
|
+
function parseFastifyRoutes(content, file) {
|
|
724
|
+
const endpoints = [];
|
|
725
|
+
const routeRegex = /(?:fastify|app)\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
726
|
+
let match;
|
|
727
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
728
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
729
|
+
endpoints.push({
|
|
730
|
+
method: match[1].toUpperCase(),
|
|
731
|
+
path: match[2],
|
|
732
|
+
file,
|
|
733
|
+
line,
|
|
734
|
+
source: 'fastify',
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
return endpoints;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Parse Hapi routes.
|
|
741
|
+
* @param content - The file content to parse.
|
|
742
|
+
* @param file - The file path.
|
|
743
|
+
* @returns Parsed API endpoints.
|
|
744
|
+
*/
|
|
745
|
+
function parseHapiRoutes(content, file) {
|
|
746
|
+
const endpoints = [];
|
|
747
|
+
const routeRegex = /(?:method|path):\s*['"](GET|POST|PUT|DELETE|PATCH|get|post|put|delete|patch)['"]|path:\s*['"]([^'"]+)['"]/gi;
|
|
748
|
+
let match;
|
|
749
|
+
let currentMethod = 'GET';
|
|
750
|
+
let currentPath = '';
|
|
751
|
+
while ((match = routeRegex.exec(content)) !== null) {
|
|
752
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
753
|
+
if (match[1]) {
|
|
754
|
+
currentMethod = match[1].toUpperCase();
|
|
755
|
+
}
|
|
756
|
+
else if (match[2]) {
|
|
757
|
+
currentPath = match[2];
|
|
758
|
+
endpoints.push({
|
|
759
|
+
method: currentMethod,
|
|
760
|
+
path: currentPath,
|
|
761
|
+
file,
|
|
762
|
+
line,
|
|
763
|
+
source: 'hapi',
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return endpoints;
|
|
768
|
+
}
|
|
183
769
|
/**
|
|
184
770
|
* Main API contract analysis function.
|
|
185
771
|
* @param cwd - The working directory to scan.
|
|
@@ -194,6 +780,12 @@ export async function analyzeApiContracts(cwd) {
|
|
|
194
780
|
const gqlFiles = await listFiles(cwd, { glob: '**/*.{graphql,gql}' });
|
|
195
781
|
const tsFiles = await listFiles(cwd, { glob: '**/*.{ts,tsx,js,jsx}' });
|
|
196
782
|
const pyFiles = await listFiles(cwd, { glob: '**/*.py' });
|
|
783
|
+
const rbFiles = await listFiles(cwd, { glob: '**/*.rb' });
|
|
784
|
+
const phpFiles = await listFiles(cwd, { glob: '**/*.php' });
|
|
785
|
+
const javaFiles = await listFiles(cwd, { glob: '**/*.java' });
|
|
786
|
+
const goFiles = await listFiles(cwd, { glob: '**/*.go' });
|
|
787
|
+
const rsFiles = await listFiles(cwd, { glob: '**/*.rs' });
|
|
788
|
+
const csFiles = await listFiles(cwd, { glob: '**/*.cs' });
|
|
197
789
|
// OpenAPI specs
|
|
198
790
|
for (const f of jsonFiles.filter((f) => /swagger|openapi/i.test(f)).slice(0, 10)) {
|
|
199
791
|
try {
|
|
@@ -224,11 +816,18 @@ export async function analyzeApiContracts(cwd) {
|
|
|
224
816
|
logger.debug(`Failed to parse possible OpenAPI spec: ${f}`, { error });
|
|
225
817
|
}
|
|
226
818
|
}
|
|
227
|
-
// Express/NestJS routes
|
|
228
|
-
|
|
819
|
+
// Express/NestJS routes - prioritize route files
|
|
820
|
+
const routeFiles = tsFiles.filter((f) => /routes?|controllers?|api|endpoints?/i.test(f) || /\.route\.(ts|js)$/i.test(f));
|
|
821
|
+
const otherTsFiles = tsFiles.filter((f) => !routeFiles.includes(f));
|
|
822
|
+
// Check route files first (more likely to contain routes)
|
|
823
|
+
for (const f of [...routeFiles, ...otherTsFiles].slice(0, 500)) {
|
|
229
824
|
try {
|
|
230
825
|
const content = await readFile(path.join(cwd, f), 'utf-8');
|
|
231
|
-
|
|
826
|
+
// Enhanced Express detection - check for multiple patterns
|
|
827
|
+
if (/(?:app|router|express\.Router)\.(get|post|put|delete|patch|all|use|route)\s*\(/i.test(content) ||
|
|
828
|
+
/express\.Router\(\)/i.test(content) ||
|
|
829
|
+
/from\s+['"]express['"]/i.test(content) ||
|
|
830
|
+
/require\s*\(['"]express['"]\)/i.test(content)) {
|
|
232
831
|
const eps = parseExpressRoutes(content, f);
|
|
233
832
|
if (eps.length > 0) {
|
|
234
833
|
allEndpoints.push(...eps);
|
|
@@ -236,6 +835,7 @@ export async function analyzeApiContracts(cwd) {
|
|
|
236
835
|
sources.add('express');
|
|
237
836
|
}
|
|
238
837
|
}
|
|
838
|
+
// NestJS detection
|
|
239
839
|
if (/@Controller/.test(content)) {
|
|
240
840
|
const eps = parseNestJSRoutes(content, f);
|
|
241
841
|
if (eps.length > 0) {
|
|
@@ -244,16 +844,45 @@ export async function analyzeApiContracts(cwd) {
|
|
|
244
844
|
sources.add('nestjs');
|
|
245
845
|
}
|
|
246
846
|
}
|
|
847
|
+
// Koa detection
|
|
848
|
+
if (/from\s+['"]koa['"]|require\s*\(['"]koa['"]/.test(content) || /router\.(get|post|put|delete)/.test(content)) {
|
|
849
|
+
const eps = parseKoaRoutes(content, f);
|
|
850
|
+
if (eps.length > 0) {
|
|
851
|
+
allEndpoints.push(...eps);
|
|
852
|
+
specFiles.push(f);
|
|
853
|
+
sources.add('koa');
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
// Fastify detection
|
|
857
|
+
if (/from\s+['"]fastify['"]|require\s*\(['"]fastify['"]/.test(content) ||
|
|
858
|
+
/fastify\.(get|post|put|delete)/.test(content)) {
|
|
859
|
+
const eps = parseFastifyRoutes(content, f);
|
|
860
|
+
if (eps.length > 0) {
|
|
861
|
+
allEndpoints.push(...eps);
|
|
862
|
+
specFiles.push(f);
|
|
863
|
+
sources.add('fastify');
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
// Hapi detection
|
|
867
|
+
if (/from\s+['"]@hapi\/hapi['"]|require\s*\(['"]@hapi\/hapi['"]/.test(content) || /server\.route/.test(content)) {
|
|
868
|
+
const eps = parseHapiRoutes(content, f);
|
|
869
|
+
if (eps.length > 0) {
|
|
870
|
+
allEndpoints.push(...eps);
|
|
871
|
+
specFiles.push(f);
|
|
872
|
+
sources.add('hapi');
|
|
873
|
+
}
|
|
874
|
+
}
|
|
247
875
|
}
|
|
248
876
|
catch (error) {
|
|
249
|
-
logger.debug(`Failed to parse possible
|
|
877
|
+
logger.debug(`Failed to parse possible route file: ${f}`, { error });
|
|
250
878
|
}
|
|
251
879
|
}
|
|
252
|
-
// FastAPI
|
|
253
|
-
for (const f of pyFiles.slice(0,
|
|
880
|
+
// Python frameworks: FastAPI, Flask, Django
|
|
881
|
+
for (const f of pyFiles.slice(0, 300)) {
|
|
254
882
|
try {
|
|
255
883
|
const content = await readFile(path.join(cwd, f), 'utf-8');
|
|
256
|
-
|
|
884
|
+
// FastAPI routes
|
|
885
|
+
if (/@(?:app|router)\.(get|post|put|delete)/.test(content) || /from\s+fastapi/.test(content)) {
|
|
257
886
|
const eps = parseFastAPIRoutes(content, f);
|
|
258
887
|
if (eps.length > 0) {
|
|
259
888
|
allEndpoints.push(...eps);
|
|
@@ -261,9 +890,199 @@ export async function analyzeApiContracts(cwd) {
|
|
|
261
890
|
sources.add('fastapi');
|
|
262
891
|
}
|
|
263
892
|
}
|
|
893
|
+
// Flask routes
|
|
894
|
+
if (/@(?:app|blueprint|router)\.(route|get|post|put|delete|patch)/.test(content) ||
|
|
895
|
+
/from\s+flask/.test(content)) {
|
|
896
|
+
const eps = parseFlaskRoutes(content, f);
|
|
897
|
+
if (eps.length > 0) {
|
|
898
|
+
allEndpoints.push(...eps);
|
|
899
|
+
specFiles.push(f);
|
|
900
|
+
sources.add('flask');
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
// Django REST Framework
|
|
904
|
+
if (/from\s+rest_framework/.test(content) || /class\s+\w+ViewSet/.test(content)) {
|
|
905
|
+
const eps = parseDjangoRoutes(content, f);
|
|
906
|
+
if (eps.length > 0) {
|
|
907
|
+
allEndpoints.push(...eps);
|
|
908
|
+
specFiles.push(f);
|
|
909
|
+
sources.add('django');
|
|
910
|
+
}
|
|
911
|
+
}
|
|
264
912
|
}
|
|
265
913
|
catch (error) {
|
|
266
|
-
logger.debug(`Failed to parse possible
|
|
914
|
+
logger.debug(`Failed to parse possible Python route file: ${f}`, { error });
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
// Ruby frameworks: Rails, Sinatra
|
|
918
|
+
const railsRouteFiles = rbFiles.filter((f) => /routes\.rb|config\/routes/.test(f));
|
|
919
|
+
const otherRbFiles = rbFiles.filter((f) => !railsRouteFiles.includes(f));
|
|
920
|
+
for (const f of [...railsRouteFiles, ...otherRbFiles].slice(0, 100)) {
|
|
921
|
+
try {
|
|
922
|
+
const content = await readFile(path.join(cwd, f), 'utf-8');
|
|
923
|
+
// Rails routes
|
|
924
|
+
if (/Rails\.application\.routes\.draw|resources?|get\s+['"]/.test(content)) {
|
|
925
|
+
const eps = parseRailsRoutes(content, f);
|
|
926
|
+
if (eps.length > 0) {
|
|
927
|
+
allEndpoints.push(...eps);
|
|
928
|
+
specFiles.push(f);
|
|
929
|
+
sources.add('rails');
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
// Sinatra routes
|
|
933
|
+
if (/require\s+['"]sinatra['"]|class\s+\w+\s*<\s*Sinatra/.test(content)) {
|
|
934
|
+
const eps = parseSinatraRoutes(content, f);
|
|
935
|
+
if (eps.length > 0) {
|
|
936
|
+
allEndpoints.push(...eps);
|
|
937
|
+
specFiles.push(f);
|
|
938
|
+
sources.add('sinatra');
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
logger.debug(`Failed to parse possible Ruby route file: ${f}`, { error });
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// PHP frameworks: Laravel
|
|
947
|
+
for (const f of phpFiles.filter((f) => /routes|Route::/.test(f)).slice(0, 50)) {
|
|
948
|
+
try {
|
|
949
|
+
const content = await readFile(path.join(cwd, f), 'utf-8');
|
|
950
|
+
if (/Route::(get|post|put|delete|resource)/.test(content)) {
|
|
951
|
+
const eps = parseLaravelRoutes(content, f);
|
|
952
|
+
if (eps.length > 0) {
|
|
953
|
+
allEndpoints.push(...eps);
|
|
954
|
+
specFiles.push(f);
|
|
955
|
+
sources.add('laravel');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
catch (error) {
|
|
960
|
+
logger.debug(`Failed to parse possible Laravel route file: ${f}`, { error });
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
// Java frameworks: Spring Boot, JAX-RS
|
|
964
|
+
for (const f of javaFiles.filter((f) => /controller|Controller|RestController|@Path/.test(f)).slice(0, 200)) {
|
|
965
|
+
try {
|
|
966
|
+
const content = await readFile(path.join(cwd, f), 'utf-8');
|
|
967
|
+
// Spring Boot
|
|
968
|
+
if (/@(?:RestController|Controller)/.test(content)) {
|
|
969
|
+
const eps = parseSpringRoutes(content, f);
|
|
970
|
+
if (eps.length > 0) {
|
|
971
|
+
allEndpoints.push(...eps);
|
|
972
|
+
specFiles.push(f);
|
|
973
|
+
sources.add('spring');
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
// JAX-RS
|
|
977
|
+
if (/@Path/.test(content) && /javax\.ws\.rs|jakarta\.ws\.rs/.test(content)) {
|
|
978
|
+
const eps = parseJAXRSRoutes(content, f);
|
|
979
|
+
if (eps.length > 0) {
|
|
980
|
+
allEndpoints.push(...eps);
|
|
981
|
+
specFiles.push(f);
|
|
982
|
+
sources.add('jaxrs');
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
catch (error) {
|
|
987
|
+
logger.debug(`Failed to parse possible Java route file: ${f}`, { error });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
// Go frameworks: Gin, Echo, Fiber, Chi
|
|
991
|
+
for (const f of goFiles.filter((f) => /routes?|handlers?|api/.test(f) || !/test/.test(f)).slice(0, 200)) {
|
|
992
|
+
try {
|
|
993
|
+
const content = await readFile(path.join(cwd, f), 'utf-8');
|
|
994
|
+
// Gin
|
|
995
|
+
if (/github\.com\/gin-gonic\/gin/.test(content) || /router\.(GET|POST|PUT|DELETE)/.test(content)) {
|
|
996
|
+
const eps = parseGinRoutes(content, f);
|
|
997
|
+
if (eps.length > 0) {
|
|
998
|
+
allEndpoints.push(...eps);
|
|
999
|
+
specFiles.push(f);
|
|
1000
|
+
sources.add('gin');
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
// Echo
|
|
1004
|
+
if (/github\.com\/labstack\/echo/.test(content) || /e\.(GET|POST|PUT|DELETE)/.test(content)) {
|
|
1005
|
+
const eps = parseEchoRoutes(content, f);
|
|
1006
|
+
if (eps.length > 0) {
|
|
1007
|
+
allEndpoints.push(...eps);
|
|
1008
|
+
specFiles.push(f);
|
|
1009
|
+
sources.add('echo');
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
// Fiber
|
|
1013
|
+
if (/github\.com\/gofiber\/fiber/.test(content) || /app\.(Get|Post|Put|Delete)/.test(content)) {
|
|
1014
|
+
const eps = parseFiberRoutes(content, f);
|
|
1015
|
+
if (eps.length > 0) {
|
|
1016
|
+
allEndpoints.push(...eps);
|
|
1017
|
+
specFiles.push(f);
|
|
1018
|
+
sources.add('fiber');
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
// Chi
|
|
1022
|
+
if (/github\.com\/go-chi\/chi/.test(content) || /r\.(Get|Post|Put|Delete)/.test(content)) {
|
|
1023
|
+
const eps = parseChiRoutes(content, f);
|
|
1024
|
+
if (eps.length > 0) {
|
|
1025
|
+
allEndpoints.push(...eps);
|
|
1026
|
+
specFiles.push(f);
|
|
1027
|
+
sources.add('chi');
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
catch (error) {
|
|
1032
|
+
logger.debug(`Failed to parse possible Go route file: ${f}`, { error });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
// Rust frameworks: Actix-web, Rocket, Axum
|
|
1036
|
+
for (const f of rsFiles.filter((f) => /routes?|handlers?|api|main/.test(f) || !/test/.test(f)).slice(0, 200)) {
|
|
1037
|
+
try {
|
|
1038
|
+
const content = await readFile(path.join(cwd, f), 'utf-8');
|
|
1039
|
+
// Actix-web
|
|
1040
|
+
if (/actix_web/.test(content) || /\.route\(/.test(content)) {
|
|
1041
|
+
const eps = parseActixRoutes(content, f);
|
|
1042
|
+
if (eps.length > 0) {
|
|
1043
|
+
allEndpoints.push(...eps);
|
|
1044
|
+
specFiles.push(f);
|
|
1045
|
+
sources.add('actix');
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
// Rocket
|
|
1049
|
+
if (/rocket/.test(content) || /#\[(get|post|put|delete)/.test(content)) {
|
|
1050
|
+
const eps = parseRocketRoutes(content, f);
|
|
1051
|
+
if (eps.length > 0) {
|
|
1052
|
+
allEndpoints.push(...eps);
|
|
1053
|
+
specFiles.push(f);
|
|
1054
|
+
sources.add('rocket');
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// Axum
|
|
1058
|
+
if (/axum/.test(content) || /Router::new/.test(content)) {
|
|
1059
|
+
const eps = parseAxumRoutes(content, f);
|
|
1060
|
+
if (eps.length > 0) {
|
|
1061
|
+
allEndpoints.push(...eps);
|
|
1062
|
+
specFiles.push(f);
|
|
1063
|
+
sources.add('axum');
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
catch (error) {
|
|
1068
|
+
logger.debug(`Failed to parse possible Rust route file: ${f}`, { error });
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
// C# frameworks: ASP.NET Core
|
|
1072
|
+
for (const f of csFiles.filter((f) => /controller|Controller/.test(f)).slice(0, 200)) {
|
|
1073
|
+
try {
|
|
1074
|
+
const content = await readFile(path.join(cwd, f), 'utf-8');
|
|
1075
|
+
if (/\[ApiController\]|class\s+\w+Controller/.test(content)) {
|
|
1076
|
+
const eps = parseAspNetRoutes(content, f);
|
|
1077
|
+
if (eps.length > 0) {
|
|
1078
|
+
allEndpoints.push(...eps);
|
|
1079
|
+
specFiles.push(f);
|
|
1080
|
+
sources.add('aspnet');
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
catch (error) {
|
|
1085
|
+
logger.debug(`Failed to parse possible C# route file: ${f}`, { error });
|
|
267
1086
|
}
|
|
268
1087
|
}
|
|
269
1088
|
// Also check for GraphQL in TS/JS files
|
|
@@ -304,8 +1123,15 @@ export async function analyzeApiContracts(cwd) {
|
|
|
304
1123
|
const scannedPatterns = [
|
|
305
1124
|
'**/*.{json,yaml,yml} (OpenAPI/Swagger containing swagger|openapi)',
|
|
306
1125
|
'**/*.{graphql,gql} (GraphQL schemas)',
|
|
307
|
-
'
|
|
308
|
-
'**/*.
|
|
1126
|
+
'**/routes/**/*.{ts,tsx,js,jsx} (Express route files - prioritized)',
|
|
1127
|
+
'**/*.{ts,tsx,js,jsx} (Express/NestJS/Koa/Fastify/Hapi routes)',
|
|
1128
|
+
'**/*.py (FastAPI/Flask/Django REST Framework routes)',
|
|
1129
|
+
'**/*.rb (Rails routes.rb, Sinatra routes)',
|
|
1130
|
+
'**/*.php (Laravel Route::get/post/resource)',
|
|
1131
|
+
'**/*.java (Spring Boot @RequestMapping, JAX-RS @Path)',
|
|
1132
|
+
'**/*.go (Gin/Echo/Fiber/Chi routes)',
|
|
1133
|
+
'**/*.rs (Actix-web/Rocket/Axum routes)',
|
|
1134
|
+
'**/*.cs (ASP.NET Core [ApiController])',
|
|
309
1135
|
];
|
|
310
1136
|
return {
|
|
311
1137
|
endpoints: allEndpoints,
|