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