@merlean/analyzer 2.3.0 → 3.0.1
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/README.md +75 -0
- package/bin/cli.js +51 -171
- package/package.json +10 -24
- package/bin/wrapper.js +0 -51
- package/lib/analyzer.js +0 -1344
- package/scripts/postinstall.js +0 -156
package/lib/analyzer.js
DELETED
|
@@ -1,1344 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Language & Framework Agnostic Codebase Scanner
|
|
3
|
-
*
|
|
4
|
-
* Scans ANY codebase (frontend or backend, any language) to extract API patterns:
|
|
5
|
-
*
|
|
6
|
-
* JavaScript/TypeScript:
|
|
7
|
-
* - Express/Fastify/Koa/Hapi route definitions
|
|
8
|
-
* - NestJS decorators (@Get, @Post, @Body, @Query, etc.)
|
|
9
|
-
* - Frontend HTTP calls (fetch, axios, HttpClient, etc.)
|
|
10
|
-
*
|
|
11
|
-
* PHP:
|
|
12
|
-
* - CodeIgniter routes ($routes->get(), $routes->post())
|
|
13
|
-
* - Laravel routes (Route::get(), Route::post())
|
|
14
|
-
* - Symfony annotations (@Route)
|
|
15
|
-
*
|
|
16
|
-
* Python:
|
|
17
|
-
* - Flask routes (@app.route)
|
|
18
|
-
* - FastAPI routes (@app.get, @router.post)
|
|
19
|
-
* - Django URLs (path(), url())
|
|
20
|
-
*
|
|
21
|
-
* Ruby:
|
|
22
|
-
* - Rails routes (get, post, resources)
|
|
23
|
-
* - Sinatra routes (get '/path')
|
|
24
|
-
*
|
|
25
|
-
* Go:
|
|
26
|
-
* - Gin routes (r.GET, r.POST)
|
|
27
|
-
* - Echo routes (e.GET, e.POST)
|
|
28
|
-
* - Chi routes (r.Get, r.Post)
|
|
29
|
-
*
|
|
30
|
-
* Java/Kotlin:
|
|
31
|
-
* - Spring Boot (@GetMapping, @PostMapping, @RequestMapping)
|
|
32
|
-
*
|
|
33
|
-
* Plus: Swagger/OpenAPI annotations, request body schemas, DTOs
|
|
34
|
-
*
|
|
35
|
-
* The goal is to understand what APIs exist, their parameters,
|
|
36
|
-
* and body structures - regardless of framework, language, or code style.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
const fs = require('fs');
|
|
40
|
-
const path = require('path');
|
|
41
|
-
const { glob } = require('glob');
|
|
42
|
-
|
|
43
|
-
// File patterns to scan - include ALL common backend/frontend languages
|
|
44
|
-
const FILE_PATTERNS = [
|
|
45
|
-
// JavaScript/TypeScript
|
|
46
|
-
'**/*.js',
|
|
47
|
-
'**/*.jsx',
|
|
48
|
-
'**/*.ts',
|
|
49
|
-
'**/*.tsx',
|
|
50
|
-
'**/*.vue',
|
|
51
|
-
'**/*.svelte',
|
|
52
|
-
'**/*.mjs',
|
|
53
|
-
'**/*.cjs',
|
|
54
|
-
// PHP
|
|
55
|
-
'**/*.php',
|
|
56
|
-
// Python
|
|
57
|
-
'**/*.py',
|
|
58
|
-
// Ruby
|
|
59
|
-
'**/*.rb',
|
|
60
|
-
// Go
|
|
61
|
-
'**/*.go',
|
|
62
|
-
// Java/Kotlin
|
|
63
|
-
'**/*.java',
|
|
64
|
-
'**/*.kt',
|
|
65
|
-
// C#
|
|
66
|
-
'**/*.cs',
|
|
67
|
-
// Rust
|
|
68
|
-
'**/*.rs'
|
|
69
|
-
];
|
|
70
|
-
|
|
71
|
-
// Only ignore truly irrelevant directories
|
|
72
|
-
const IGNORE_PATTERNS = [
|
|
73
|
-
'**/node_modules/**',
|
|
74
|
-
'**/vendor/**',
|
|
75
|
-
'**/.git/**',
|
|
76
|
-
'**/dist/**',
|
|
77
|
-
'**/build/**',
|
|
78
|
-
'**/coverage/**',
|
|
79
|
-
'**/__pycache__/**',
|
|
80
|
-
'**/venv/**',
|
|
81
|
-
'**/*.min.js',
|
|
82
|
-
'**/*.map',
|
|
83
|
-
'**/*.d.ts', // TypeScript declaration files
|
|
84
|
-
'**/*.spec.ts', // Test files
|
|
85
|
-
'**/*.spec.js',
|
|
86
|
-
'**/*.test.ts',
|
|
87
|
-
'**/*.test.js',
|
|
88
|
-
'**/__tests__/**',
|
|
89
|
-
'**/__mocks__/**'
|
|
90
|
-
];
|
|
91
|
-
|
|
92
|
-
// Files that are highly likely to contain API definitions (any language)
|
|
93
|
-
const HIGH_PRIORITY_PATTERNS = [
|
|
94
|
-
// Generic route/controller patterns (any extension)
|
|
95
|
-
/routes?\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
|
|
96
|
-
/router\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
|
|
97
|
-
/controller\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
|
|
98
|
-
/\.controller\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
|
|
99
|
-
/Controller\.(php|java|kt|cs)$/, // PascalCase controllers (PHP, Java, C#)
|
|
100
|
-
/service\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
|
|
101
|
-
/\.service\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
|
|
102
|
-
/api\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
|
|
103
|
-
/endpoints?\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
|
|
104
|
-
/http\.(ts|js|php|py|rb|go)$/i,
|
|
105
|
-
/client\.(ts|js|php|py|rb|go)$/i,
|
|
106
|
-
|
|
107
|
-
// TypeScript/JavaScript specific
|
|
108
|
-
/dto\.ts$/i,
|
|
109
|
-
/\.dto\.ts$/i,
|
|
110
|
-
/interfaces?\.ts$/i,
|
|
111
|
-
/types?\.ts$/i,
|
|
112
|
-
/schema\.ts$/i,
|
|
113
|
-
/schemas?\.ts$/i,
|
|
114
|
-
/validation\.ts$/i,
|
|
115
|
-
|
|
116
|
-
// PHP specific (CodeIgniter, Laravel, Symfony)
|
|
117
|
-
/Routes\.php$/i,
|
|
118
|
-
/web\.php$/i, // Laravel web routes
|
|
119
|
-
/api\.php$/i, // Laravel API routes
|
|
120
|
-
/Config\/Routes\.php$/i, // CodeIgniter routes
|
|
121
|
-
// CodeIgniter convention: application/controllers/**/*.php are ALL route controllers
|
|
122
|
-
/application\/controllers\/.*\.php$/i,
|
|
123
|
-
/app\/Controllers\/.*\.php$/i, // CodeIgniter 4
|
|
124
|
-
/app\/Http\/Controllers\/.*\.php$/i, // Laravel controllers
|
|
125
|
-
// Libraries and models that define data structures
|
|
126
|
-
/application\/libraries\/.*\.php$/i,
|
|
127
|
-
/application\/models\/.*\.php$/i,
|
|
128
|
-
/app\/Models\/.*\.php$/i,
|
|
129
|
-
/app\/Services\/.*\.php$/i,
|
|
130
|
-
|
|
131
|
-
// Python specific (Flask, FastAPI, Django)
|
|
132
|
-
/views?\.py$/i,
|
|
133
|
-
/urls\.py$/i, // Django URLs
|
|
134
|
-
/routers?\.py$/i, // FastAPI routers
|
|
135
|
-
|
|
136
|
-
// Ruby specific (Rails)
|
|
137
|
-
/routes\.rb$/i,
|
|
138
|
-
/_controller\.rb$/i,
|
|
139
|
-
|
|
140
|
-
// Go specific
|
|
141
|
-
/handlers?\.go$/i,
|
|
142
|
-
/routes?\.go$/i,
|
|
143
|
-
|
|
144
|
-
// Java/Kotlin specific (Spring Boot)
|
|
145
|
-
/Controller\.java$/,
|
|
146
|
-
/Controller\.kt$/,
|
|
147
|
-
/RestController\.java$/,
|
|
148
|
-
/RestController\.kt$/
|
|
149
|
-
];
|
|
150
|
-
|
|
151
|
-
// Medium priority - might contain API patterns
|
|
152
|
-
const MEDIUM_PRIORITY_PATTERNS = [
|
|
153
|
-
/index\.(ts|js|php|py|rb|go)$/i,
|
|
154
|
-
/app\.(ts|js|php|py|rb|go)$/i,
|
|
155
|
-
/main\.(ts|js|php|py|rb|go|java|kt)$/i,
|
|
156
|
-
/server\.(ts|js|php|py|rb|go)$/i,
|
|
157
|
-
/Application\.(java|kt)$/, // Spring Boot main class
|
|
158
|
-
/bootstrap\.php$/i, // PHP bootstrap files
|
|
159
|
-
/kernel\.php$/i, // Laravel/Symfony kernel
|
|
160
|
-
];
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Scan codebase and collect API patterns from any framework
|
|
164
|
-
*/
|
|
165
|
-
async function scanCodebase(codebasePath) {
|
|
166
|
-
console.log(' Scanning files...');
|
|
167
|
-
|
|
168
|
-
// Get all matching files
|
|
169
|
-
const files = await glob(FILE_PATTERNS, {
|
|
170
|
-
cwd: codebasePath,
|
|
171
|
-
ignore: IGNORE_PATTERNS,
|
|
172
|
-
absolute: true
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
console.log(` Found ${files.length} files`);
|
|
176
|
-
|
|
177
|
-
// Categorize and prioritize files
|
|
178
|
-
const { highPriority, mediumPriority, other } = categorizeFiles(files, codebasePath);
|
|
179
|
-
|
|
180
|
-
console.log(` High priority: ${highPriority.length}, Medium: ${mediumPriority.length}, Other: ${other.length}`);
|
|
181
|
-
|
|
182
|
-
// Analyze high priority files first (routes, controllers, services, DTOs)
|
|
183
|
-
// Then medium priority, then scan others for API patterns
|
|
184
|
-
// Increased limits to ensure all controllers are scanned
|
|
185
|
-
const filesToAnalyze = [
|
|
186
|
-
...highPriority, // ALL high priority files (controllers, routes, etc.)
|
|
187
|
-
...mediumPriority.slice(0, 30),
|
|
188
|
-
...other.slice(0, 75)
|
|
189
|
-
];
|
|
190
|
-
|
|
191
|
-
console.log(` Analyzing ${filesToAnalyze.length} files for API patterns...`);
|
|
192
|
-
|
|
193
|
-
// PHASE 1: Pre-scan all files to discover JSON structures
|
|
194
|
-
// This ensures structures found in one file can be used for others
|
|
195
|
-
console.log(` Phase 1: Discovering JSON structures...`);
|
|
196
|
-
for (const file of filesToAnalyze) {
|
|
197
|
-
try {
|
|
198
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
199
|
-
// Look for structure-defining patterns (json_decode + array access)
|
|
200
|
-
preDiscoverStructures(content);
|
|
201
|
-
} catch (error) {
|
|
202
|
-
// Skip files that can't be read
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
console.log(` Discovered ${discoveredStructures.size} reusable structures`);
|
|
206
|
-
|
|
207
|
-
// PHASE 2: Extract routes and apply discovered structures
|
|
208
|
-
const fileContents = [];
|
|
209
|
-
const preExtractedRoutes = []; // Routes extracted directly from conventions
|
|
210
|
-
let filesWithPatterns = 0;
|
|
211
|
-
|
|
212
|
-
for (const file of filesToAnalyze) {
|
|
213
|
-
try {
|
|
214
|
-
const content = fs.readFileSync(file, 'utf-8');
|
|
215
|
-
const relativePath = path.relative(codebasePath, file);
|
|
216
|
-
|
|
217
|
-
// Pre-extract routes from convention-based frameworks (CodeIgniter, etc.)
|
|
218
|
-
const conventionRoutes = extractConventionRoutes(content, relativePath);
|
|
219
|
-
if (conventionRoutes.length > 0) {
|
|
220
|
-
preExtractedRoutes.push(...conventionRoutes);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Extract API patterns from the file
|
|
224
|
-
const extracted = extractApiPatterns(content, relativePath, file);
|
|
225
|
-
|
|
226
|
-
if (extracted.hasApiPatterns) {
|
|
227
|
-
filesWithPatterns++;
|
|
228
|
-
fileContents.push({
|
|
229
|
-
path: relativePath,
|
|
230
|
-
content: extracted.content
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
} catch (error) {
|
|
234
|
-
// Skip files that can't be read
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
console.log(` Found API patterns in ${filesWithPatterns} files`);
|
|
239
|
-
if (preExtractedRoutes.length > 0) {
|
|
240
|
-
console.log(` Pre-extracted ${preExtractedRoutes.length} convention-based routes`);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return { files: fileContents, preExtractedRoutes };
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Extract routes from convention-based frameworks
|
|
248
|
-
* Returns array of {method, path, description, bodySchema?} objects
|
|
249
|
-
*/
|
|
250
|
-
function extractConventionRoutes(content, filePath) {
|
|
251
|
-
const routes = [];
|
|
252
|
-
|
|
253
|
-
// CodeIgniter: application/controllers/api/v2/Settings.php -> /api/v2/settings
|
|
254
|
-
const basePath = extractRouteFromControllerPath(filePath);
|
|
255
|
-
if (!basePath) return routes;
|
|
256
|
-
|
|
257
|
-
// Split content into lines for method body extraction
|
|
258
|
-
const lines = content.split('\n');
|
|
259
|
-
|
|
260
|
-
// Extract CodeIgniter REST controller methods: index_get, index_post, items_get, etc.
|
|
261
|
-
const methodRegex = /public\s+function\s+(\w+)_(get|post|put|patch|delete)\s*\(([^)]*)\)/gi;
|
|
262
|
-
let match;
|
|
263
|
-
|
|
264
|
-
while ((match = methodRegex.exec(content)) !== null) {
|
|
265
|
-
const action = match[1]; // e.g., 'index', 'items', 'user'
|
|
266
|
-
const httpMethod = match[2].toUpperCase(); // e.g., 'GET', 'POST'
|
|
267
|
-
const params = match[3]; // e.g., '$id = null'
|
|
268
|
-
const matchIndex = match.index;
|
|
269
|
-
|
|
270
|
-
// Build the route path
|
|
271
|
-
let path = basePath;
|
|
272
|
-
if (action !== 'index') {
|
|
273
|
-
path += '/' + action.toLowerCase().replace(/_/g, '/');
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Add path parameters from function arguments
|
|
277
|
-
if (params) {
|
|
278
|
-
const paramMatches = params.match(/\$(\w+)(?:\s*=\s*[^,)]+)?/g);
|
|
279
|
-
if (paramMatches) {
|
|
280
|
-
paramMatches.forEach(p => {
|
|
281
|
-
const paramName = p.match(/\$(\w+)/)[1];
|
|
282
|
-
// Only add as path param if it's not optional (no default value) or looks like an ID
|
|
283
|
-
if (!p.includes('=') || paramName.toLowerCase().includes('id')) {
|
|
284
|
-
path += '/:' + paramName;
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Extract method body to analyze for body schema (for POST/PUT/PATCH)
|
|
291
|
-
let bodySchema = null;
|
|
292
|
-
if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
|
|
293
|
-
bodySchema = extractBodySchemaFromMethod(content, matchIndex);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const route = {
|
|
297
|
-
method: httpMethod,
|
|
298
|
-
path,
|
|
299
|
-
description: `${action}_${match[2]}() method in ${filePath.split('/').pop()}`
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
if (bodySchema && Object.keys(bodySchema).length > 0) {
|
|
303
|
-
route.bodySchema = bodySchema;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
routes.push(route);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return routes;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Global cache for discovered complex structures (shared across file analysis)
|
|
313
|
-
const discoveredStructures = new Map();
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Pre-scan a file to discover JSON structure patterns
|
|
317
|
-
* Looks for json_decode followed by array access patterns
|
|
318
|
-
*/
|
|
319
|
-
function preDiscoverStructures(content) {
|
|
320
|
-
// Find patterns like: $actions = json_decode($actions, true);
|
|
321
|
-
const jsonDecodeRegex = /\$(\w+)\s*=\s*json_decode\s*\(\s*\$\1/gi;
|
|
322
|
-
let match;
|
|
323
|
-
|
|
324
|
-
while ((match = jsonDecodeRegex.exec(content)) !== null) {
|
|
325
|
-
const varName = match[1];
|
|
326
|
-
|
|
327
|
-
// Extract the structure used for this variable
|
|
328
|
-
const structure = extractNestedStructure(content, varName);
|
|
329
|
-
|
|
330
|
-
// If we found a meaningful structure with examples, cache it
|
|
331
|
-
if (structure._example && Object.keys(structure._example).length > 0) {
|
|
332
|
-
// Determine field name from common patterns
|
|
333
|
-
// e.g., $actions = $this->post('actions'); $actions = json_decode($actions);
|
|
334
|
-
const fieldRegex = new RegExp(
|
|
335
|
-
`\\$${varName}\\s*=\\s*\\$this\\s*->\\s*(?:post|put|patch|get)\\s*\\(\\s*['"]([\\w]+)['"]`,
|
|
336
|
-
'i'
|
|
337
|
-
);
|
|
338
|
-
const fieldMatch = content.match(fieldRegex);
|
|
339
|
-
if (fieldMatch) {
|
|
340
|
-
const fieldName = fieldMatch[1];
|
|
341
|
-
discoveredStructures.set(fieldName, structure);
|
|
342
|
-
} else {
|
|
343
|
-
// Use variable name as field name
|
|
344
|
-
discoveredStructures.set(varName, structure);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Also look for library/model calls that reveal structure
|
|
350
|
-
// e.g., ->edit($update["settings"]), ->edit($update["integrations"])
|
|
351
|
-
const modelEditRegex = /(\w+_model|lib_\w+)\s*->\s*edit\s*\(\s*\$(\w+)\s*\[\s*["'](\w+)["']\s*\]/gi;
|
|
352
|
-
const discoveredSections = new Set();
|
|
353
|
-
|
|
354
|
-
while ((match = modelEditRegex.exec(content)) !== null) {
|
|
355
|
-
const section = match[3]; // e.g., 'settings', 'integrations', 'assignments'
|
|
356
|
-
discoveredSections.add(section);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Store discovered sections globally so they can be applied later
|
|
360
|
-
if (discoveredSections.size > 0) {
|
|
361
|
-
const existingSections = discoveredStructures.get('_sections') || new Set();
|
|
362
|
-
for (const section of discoveredSections) {
|
|
363
|
-
existingSections.add(section);
|
|
364
|
-
}
|
|
365
|
-
discoveredStructures.set('_sections', existingSections);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Also look for common field names with library calls
|
|
369
|
-
// e.g., $this->lib_settings->edit($actions) suggests 'actions' has a structure
|
|
370
|
-
const libCallRegex = /lib_settings\s*->\s*edit\s*\(\s*\$(\w+)/gi;
|
|
371
|
-
while ((match = libCallRegex.exec(content)) !== null) {
|
|
372
|
-
const varName = match[1];
|
|
373
|
-
// If we already discovered structure for 'actions', this confirms it
|
|
374
|
-
if (!discoveredStructures.has(varName)) {
|
|
375
|
-
// Mark it as a known complex field even if we don't have the structure
|
|
376
|
-
const existing = discoveredStructures.get('actions');
|
|
377
|
-
if (existing) {
|
|
378
|
-
discoveredStructures.set(varName, existing);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Extract body schema from a CodeIgniter controller method
|
|
386
|
-
* Looks for $this->post('field'), $this->input->post('field'), $this->put('field'), etc.
|
|
387
|
-
* Also analyzes nested array access patterns after json_decode
|
|
388
|
-
*/
|
|
389
|
-
function extractBodySchemaFromMethod(content, methodStartIndex, fullFileContent = null) {
|
|
390
|
-
const bodyFields = {};
|
|
391
|
-
|
|
392
|
-
// Find the method body (from method start to next public function or end of class)
|
|
393
|
-
const methodContent = content.slice(methodStartIndex, methodStartIndex + 8000); // Increased limit
|
|
394
|
-
const endMatch = methodContent.match(/\n\s*(?:public|private|protected)\s+function\s+/);
|
|
395
|
-
const methodBody = endMatch ? methodContent.slice(0, endMatch.index) : methodContent;
|
|
396
|
-
|
|
397
|
-
// Use full file content for structure analysis if available
|
|
398
|
-
const analysisContent = fullFileContent || content;
|
|
399
|
-
|
|
400
|
-
// CodeIgniter REST: $this->post('field'), $this->put('field'), $this->patch('field')
|
|
401
|
-
const postRegex = /\$(\w+)\s*=\s*\$this\s*->\s*(post|put|patch|get)\s*\(\s*['"](\w+)['"]/gi;
|
|
402
|
-
let match;
|
|
403
|
-
while ((match = postRegex.exec(methodBody)) !== null) {
|
|
404
|
-
const varName = match[1];
|
|
405
|
-
const fieldName = match[3];
|
|
406
|
-
|
|
407
|
-
// Check if this is a complex field that likely contains JSON structure
|
|
408
|
-
const isComplexField = isLikelyJsonField(fieldName);
|
|
409
|
-
|
|
410
|
-
// Check if this var is later json_decoded OR is a known complex field
|
|
411
|
-
const jsonDecodeCheck = new RegExp(`\\$${varName}\\s*=\\s*json_decode\\s*\\(\\s*\\$${varName}`, 'i');
|
|
412
|
-
const hasJsonDecode = jsonDecodeCheck.test(methodBody);
|
|
413
|
-
|
|
414
|
-
if (hasJsonDecode || isComplexField) {
|
|
415
|
-
// Analyze nested structure for this variable
|
|
416
|
-
let nestedSchema = extractNestedStructure(analysisContent, varName);
|
|
417
|
-
|
|
418
|
-
// If we found structure, cache it for reuse
|
|
419
|
-
if (Object.keys(nestedSchema).length > 0 && !nestedSchema._example) {
|
|
420
|
-
// Has keys but no example - partial structure
|
|
421
|
-
} else if (Object.keys(nestedSchema).length > 0) {
|
|
422
|
-
discoveredStructures.set(fieldName, nestedSchema);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// If we didn't find structure locally, check cache
|
|
426
|
-
if (Object.keys(nestedSchema).length === 0 || !nestedSchema._example) {
|
|
427
|
-
const cachedStructure = discoveredStructures.get(fieldName);
|
|
428
|
-
if (cachedStructure) {
|
|
429
|
-
nestedSchema = cachedStructure;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (Object.keys(nestedSchema).length > 0) {
|
|
434
|
-
bodyFields[fieldName] = nestedSchema;
|
|
435
|
-
} else {
|
|
436
|
-
bodyFields[fieldName] = 'object (JSON structure)';
|
|
437
|
-
}
|
|
438
|
-
} else {
|
|
439
|
-
bodyFields[fieldName] = inferFieldType(fieldName, methodBody);
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Simple post without assignment
|
|
444
|
-
const simplePostRegex = /\$this\s*->\s*(post|put|patch|get)\s*\(\s*['"](\w+)['"]/gi;
|
|
445
|
-
while ((match = simplePostRegex.exec(methodBody)) !== null) {
|
|
446
|
-
const fieldName = match[2];
|
|
447
|
-
if (!bodyFields[fieldName]) {
|
|
448
|
-
// Check if this is a known complex field
|
|
449
|
-
if (isLikelyJsonField(fieldName)) {
|
|
450
|
-
const cachedStructure = discoveredStructures.get(fieldName);
|
|
451
|
-
if (cachedStructure) {
|
|
452
|
-
bodyFields[fieldName] = cachedStructure;
|
|
453
|
-
} else {
|
|
454
|
-
bodyFields[fieldName] = 'object (JSON structure)';
|
|
455
|
-
}
|
|
456
|
-
} else {
|
|
457
|
-
bodyFields[fieldName] = inferFieldType(fieldName, methodBody);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// CodeIgniter 3: $this->input->post('field')
|
|
463
|
-
const inputPostRegex = /\$this\s*->\s*input\s*->\s*(post|get|put)\s*\(\s*['"](\w+)['"]/gi;
|
|
464
|
-
while ((match = inputPostRegex.exec(methodBody)) !== null) {
|
|
465
|
-
const fieldName = match[2];
|
|
466
|
-
if (!bodyFields[fieldName]) {
|
|
467
|
-
if (isLikelyJsonField(fieldName)) {
|
|
468
|
-
const cachedStructure = discoveredStructures.get(fieldName);
|
|
469
|
-
bodyFields[fieldName] = cachedStructure || 'object (JSON structure)';
|
|
470
|
-
} else {
|
|
471
|
-
bodyFields[fieldName] = inferFieldType(fieldName, methodBody);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// PHP $_POST['field'], $_GET['field'], $_REQUEST['field']
|
|
477
|
-
const globalPostRegex = /\$_(POST|GET|REQUEST|PUT)\s*\[\s*['"](\w+)['"]\s*\]/gi;
|
|
478
|
-
while ((match = globalPostRegex.exec(methodBody)) !== null) {
|
|
479
|
-
const fieldName = match[2];
|
|
480
|
-
if (!bodyFields[fieldName]) {
|
|
481
|
-
bodyFields[fieldName] = inferFieldType(fieldName, methodBody);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Laravel/PHP: $request->input('field'), $request->get('field')
|
|
486
|
-
const requestRegex = /\$request\s*->\s*(input|get|post|all)\s*\(\s*['"](\w+)['"]/gi;
|
|
487
|
-
while ((match = requestRegex.exec(methodBody)) !== null) {
|
|
488
|
-
const fieldName = match[2];
|
|
489
|
-
if (!bodyFields[fieldName]) {
|
|
490
|
-
if (isLikelyJsonField(fieldName)) {
|
|
491
|
-
const cachedStructure = discoveredStructures.get(fieldName);
|
|
492
|
-
bodyFields[fieldName] = cachedStructure || 'object (JSON structure)';
|
|
493
|
-
} else {
|
|
494
|
-
bodyFields[fieldName] = inferFieldType(fieldName, methodBody);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
return bodyFields;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Check if a field name likely contains JSON/object data
|
|
504
|
-
*/
|
|
505
|
-
function isLikelyJsonField(fieldName) {
|
|
506
|
-
const jsonFieldNames = [
|
|
507
|
-
'actions', 'data', 'body', 'payload', 'params', 'options', 'settings',
|
|
508
|
-
'config', 'meta', 'metadata', 'attributes', 'properties', 'fields',
|
|
509
|
-
'items', 'records', 'entries', 'values', 'content', 'json'
|
|
510
|
-
];
|
|
511
|
-
return jsonFieldNames.includes(fieldName.toLowerCase());
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Extract nested structure from array access patterns
|
|
516
|
-
* e.g., $actions['settings']['updates'] -> { settings: { updates: [] } }
|
|
517
|
-
*/
|
|
518
|
-
function extractNestedStructure(methodBody, varName) {
|
|
519
|
-
const structure = {};
|
|
520
|
-
const itemFields = new Set();
|
|
521
|
-
|
|
522
|
-
// Match direct access patterns like $varName['key1']['key2']
|
|
523
|
-
const arrayAccessRegex = new RegExp(
|
|
524
|
-
`\\$${varName}\\s*\\[\\s*['"]([\\w]+)['"]\\s*\\]\\s*\\[\\s*['"]([\\w]+)['"]\\s*\\]`,
|
|
525
|
-
'gi'
|
|
526
|
-
);
|
|
527
|
-
|
|
528
|
-
let match;
|
|
529
|
-
while ((match = arrayAccessRegex.exec(methodBody)) !== null) {
|
|
530
|
-
const key1 = match[1];
|
|
531
|
-
const key2 = match[2];
|
|
532
|
-
|
|
533
|
-
if (!structure[key1]) structure[key1] = {};
|
|
534
|
-
structure[key1][key2] = []; // Arrays like updates, inserts, deletes
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
// Look for foreach patterns to find nested iterators
|
|
538
|
-
// Pattern: foreach ($varName as $key => &$iterVar) { ... foreach ($iterVar['subkey'] as &$item) }
|
|
539
|
-
const foreachRegex = new RegExp(
|
|
540
|
-
`foreach\\s*\\(\\s*\\$${varName}\\s+as\\s+(?:\\$\\w+\\s*=>\\s*)?[&]?\\$(\\w+)\\s*\\)`,
|
|
541
|
-
'gi'
|
|
542
|
-
);
|
|
543
|
-
|
|
544
|
-
while ((match = foreachRegex.exec(methodBody)) !== null) {
|
|
545
|
-
const iterVar = match[1];
|
|
546
|
-
|
|
547
|
-
// Find nested foreach on iterator's array property
|
|
548
|
-
// e.g., foreach ($settings['updates'] as &$setting)
|
|
549
|
-
const nestedForeachRegex = new RegExp(
|
|
550
|
-
`foreach\\s*\\(\\s*\\$${iterVar}\\s*\\[\\s*['"]([\\w]+)['"]\\s*\\]\\s+as\\s+(?:\\$\\w+\\s*=>\\s*)?[&]?\\$(\\w+)\\s*\\)`,
|
|
551
|
-
'gi'
|
|
552
|
-
);
|
|
553
|
-
|
|
554
|
-
let nestedMatch;
|
|
555
|
-
while ((nestedMatch = nestedForeachRegex.exec(methodBody)) !== null) {
|
|
556
|
-
const arrayKey = nestedMatch[1]; // e.g., 'updates'
|
|
557
|
-
const itemVar = nestedMatch[2]; // e.g., 'setting'
|
|
558
|
-
|
|
559
|
-
// Find all field accesses on the item variable
|
|
560
|
-
const itemAccessRegex = new RegExp(`\\$${itemVar}\\s*\\[\\s*['"]([\\w]+)['"]\\s*\\]`, 'gi');
|
|
561
|
-
let itemMatch;
|
|
562
|
-
while ((itemMatch = itemAccessRegex.exec(methodBody)) !== null) {
|
|
563
|
-
itemFields.add(itemMatch[1]);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Also look for direct item field accesses (common variable names)
|
|
569
|
-
const commonItemVars = ['setting', 'item', 'update', 'insert', 'delete', 'row', 'entry', 'data', 'integration', 'assignment'];
|
|
570
|
-
for (const itemVar of commonItemVars) {
|
|
571
|
-
const itemAccessRegex = new RegExp(`\\$${itemVar}\\s*\\[\\s*['"]([\\w]+)['"]\\s*\\]`, 'gi');
|
|
572
|
-
let itemMatch;
|
|
573
|
-
while ((itemMatch = itemAccessRegex.exec(methodBody)) !== null) {
|
|
574
|
-
itemFields.add(itemMatch[1]);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// Look for common section names that might be used alongside 'settings'
|
|
579
|
-
// Pattern: $actions['sectionName'] or $actions["sectionName"]
|
|
580
|
-
const sectionRegex = new RegExp(`\\$${varName}\\s*\\[\\s*['"]([\\w]+)['"]\\s*\\]`, 'gi');
|
|
581
|
-
const sections = new Set();
|
|
582
|
-
let sectionMatch;
|
|
583
|
-
while ((sectionMatch = sectionRegex.exec(methodBody)) !== null) {
|
|
584
|
-
sections.add(sectionMatch[1]);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Common section names in settings/actions payloads
|
|
588
|
-
const commonSections = ['settings', 'assignments', 'integrations', 'notifications', 'permissions'];
|
|
589
|
-
for (const section of commonSections) {
|
|
590
|
-
// Check if this section is referenced anywhere in the codebase
|
|
591
|
-
if (methodBody.includes(`['${section}']`) || methodBody.includes(`["${section}"]`)) {
|
|
592
|
-
sections.add(section);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Add sections discovered during pre-scan phase
|
|
597
|
-
const globalSections = discoveredStructures.get('_sections');
|
|
598
|
-
if (globalSections) {
|
|
599
|
-
for (const section of globalSections) {
|
|
600
|
-
// Filter out false positives (id, id_user, etc. are not sections)
|
|
601
|
-
if (!section.startsWith('id') && section.length > 2) {
|
|
602
|
-
sections.add(section);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Build item schema from collected fields
|
|
608
|
-
const itemSchema = {};
|
|
609
|
-
for (const field of itemFields) {
|
|
610
|
-
itemSchema[field] = inferFieldType(field, methodBody);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Ensure common fields are included
|
|
614
|
-
const commonFields = ['key', 'value', 'id_facility', 'type', 'category', 'id_file', 'integrator'];
|
|
615
|
-
for (const field of commonFields) {
|
|
616
|
-
if (!itemSchema[field]) {
|
|
617
|
-
// Check if field is referenced in content
|
|
618
|
-
if (methodBody.includes(`['${field}']`) || methodBody.includes(`["${field}"]`)) {
|
|
619
|
-
itemSchema[field] = inferFieldType(field, methodBody);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Build structure for all discovered sections
|
|
625
|
-
if (sections.size > 0 && Object.keys(itemSchema).length > 0) {
|
|
626
|
-
for (const section of sections) {
|
|
627
|
-
if (!structure[section]) {
|
|
628
|
-
structure[section] = {};
|
|
629
|
-
}
|
|
630
|
-
// Add updates/inserts/deletes for each section
|
|
631
|
-
for (const action of ['updates', 'inserts', 'deletes']) {
|
|
632
|
-
structure[section][action] = `array of { ${Object.entries(itemSchema).map(([k, v]) => `${k}: ${v}`).join(', ')} }`;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
} else if (Object.keys(itemSchema).length > 0) {
|
|
636
|
-
// Apply item schema to existing structure
|
|
637
|
-
for (const [key, value] of Object.entries(structure)) {
|
|
638
|
-
if (typeof value === 'object' && !key.startsWith('_')) {
|
|
639
|
-
for (const [subKey, subValue] of Object.entries(value)) {
|
|
640
|
-
if (Array.isArray(subValue) || ['updates', 'inserts', 'deletes', 'items'].includes(subKey)) {
|
|
641
|
-
structure[key][subKey] = `array of { ${Object.entries(itemSchema).map(([k, v]) => `${k}: ${v}`).join(', ')} }`;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// Generate a complete example
|
|
649
|
-
if (Object.keys(structure).length > 0 && Object.keys(itemSchema).length > 0) {
|
|
650
|
-
const exampleItem = {};
|
|
651
|
-
for (const [field, type] of Object.entries(itemSchema)) {
|
|
652
|
-
exampleItem[field] = getExampleValue(field, type);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// Example for integration item (with integrator field)
|
|
656
|
-
const integrationExampleItem = { ...exampleItem, integrator: 'mews' };
|
|
657
|
-
|
|
658
|
-
structure._example = {};
|
|
659
|
-
for (const [key, value] of Object.entries(structure)) {
|
|
660
|
-
if (key.startsWith('_')) continue;
|
|
661
|
-
structure._example[key] = {};
|
|
662
|
-
for (const [subKey] of Object.entries(value)) {
|
|
663
|
-
if (['updates', 'inserts', 'deletes'].includes(subKey)) {
|
|
664
|
-
if (key === 'integrations') {
|
|
665
|
-
structure._example[key][subKey] = subKey === 'updates' ? [integrationExampleItem] : [];
|
|
666
|
-
} else {
|
|
667
|
-
structure._example[key][subKey] = subKey === 'updates' ? [exampleItem] : [];
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
return structure;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Generate an example JSON body based on extracted structure
|
|
679
|
-
*/
|
|
680
|
-
function generateExample(structure, itemFields) {
|
|
681
|
-
const example = {};
|
|
682
|
-
|
|
683
|
-
for (const [key, value] of Object.entries(structure)) {
|
|
684
|
-
if (key.startsWith('_')) continue;
|
|
685
|
-
|
|
686
|
-
if (typeof value === 'object' && value !== null) {
|
|
687
|
-
// Nested object
|
|
688
|
-
if (value._arrayOf) {
|
|
689
|
-
// Array of items
|
|
690
|
-
const itemExample = {};
|
|
691
|
-
for (const [itemKey, itemType] of Object.entries(value._arrayOf)) {
|
|
692
|
-
itemExample[itemKey] = getExampleValue(itemKey, itemType);
|
|
693
|
-
}
|
|
694
|
-
example[key] = [itemExample];
|
|
695
|
-
} else {
|
|
696
|
-
example[key] = generateExample(value, itemFields);
|
|
697
|
-
}
|
|
698
|
-
} else {
|
|
699
|
-
example[key] = getExampleValue(key, value);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
return example;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
/**
|
|
707
|
-
* Get example value for a field based on its name and type
|
|
708
|
-
*/
|
|
709
|
-
function getExampleValue(fieldName, fieldType) {
|
|
710
|
-
const nameLower = fieldName.toLowerCase();
|
|
711
|
-
|
|
712
|
-
if (nameLower === 'key') return 'hotel_title';
|
|
713
|
-
if (nameLower === 'value') return 'My Hotel Name';
|
|
714
|
-
if (nameLower === 'id_facility' || nameLower === 'id_file') return '-1';
|
|
715
|
-
if (nameLower === 'type') return 'text';
|
|
716
|
-
if (nameLower === 'category') return 'main';
|
|
717
|
-
if (nameLower === 'id') return 1;
|
|
718
|
-
if (nameLower.includes('email')) return 'user@example.com';
|
|
719
|
-
if (fieldType === 'integer' || fieldType === 'number') return 1;
|
|
720
|
-
if (fieldType === 'boolean') return true;
|
|
721
|
-
if (fieldType === 'array') return [];
|
|
722
|
-
|
|
723
|
-
return 'string_value';
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* Infer field type from field name and context
|
|
728
|
-
*/
|
|
729
|
-
function inferFieldType(fieldName, context) {
|
|
730
|
-
const nameLower = fieldName.toLowerCase();
|
|
731
|
-
|
|
732
|
-
// ID fields
|
|
733
|
-
if (nameLower.startsWith('id_') || nameLower.endsWith('_id') || nameLower === 'id') {
|
|
734
|
-
return 'integer';
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// Boolean-ish fields
|
|
738
|
-
if (nameLower.startsWith('is_') || nameLower.startsWith('has_') ||
|
|
739
|
-
nameLower.includes('enabled') || nameLower.includes('active') ||
|
|
740
|
-
nameLower.includes('flag') || nameLower === 'status') {
|
|
741
|
-
return 'boolean';
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// Date/time fields
|
|
745
|
-
if (nameLower.includes('date') || nameLower.includes('time') ||
|
|
746
|
-
nameLower.includes('_at') || nameLower === 'created' || nameLower === 'updated') {
|
|
747
|
-
return 'datetime';
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Numeric fields
|
|
751
|
-
if (nameLower.includes('count') || nameLower.includes('amount') ||
|
|
752
|
-
nameLower.includes('price') || nameLower.includes('quantity') ||
|
|
753
|
-
nameLower.includes('total') || nameLower.includes('number')) {
|
|
754
|
-
return 'number';
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
// Email
|
|
758
|
-
if (nameLower.includes('email')) {
|
|
759
|
-
return 'email';
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// Array/JSON fields
|
|
763
|
-
if (nameLower.includes('items') || nameLower.includes('list') ||
|
|
764
|
-
nameLower.includes('array') || nameLower === 'data' || nameLower === 'actions') {
|
|
765
|
-
return 'array|object';
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// Default to string
|
|
769
|
-
return 'string';
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
/**
|
|
773
|
-
* Categorize files by priority
|
|
774
|
-
*/
|
|
775
|
-
function categorizeFiles(files, basePath) {
|
|
776
|
-
const highPriority = [];
|
|
777
|
-
const mediumPriority = [];
|
|
778
|
-
const other = [];
|
|
779
|
-
|
|
780
|
-
for (const file of files) {
|
|
781
|
-
const relativePath = path.relative(basePath, file);
|
|
782
|
-
|
|
783
|
-
if (HIGH_PRIORITY_PATTERNS.some(p => p.test(relativePath))) {
|
|
784
|
-
highPriority.push(file);
|
|
785
|
-
} else if (MEDIUM_PRIORITY_PATTERNS.some(p => p.test(relativePath))) {
|
|
786
|
-
mediumPriority.push(file);
|
|
787
|
-
} else {
|
|
788
|
-
other.push(file);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
return { highPriority, mediumPriority, other };
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Extract Swagger/OpenAPI documentation blocks
|
|
797
|
-
*/
|
|
798
|
-
function extractSwaggerBlocks(content) {
|
|
799
|
-
const swaggerBlocks = [];
|
|
800
|
-
|
|
801
|
-
// Match JSDoc blocks with @swagger
|
|
802
|
-
const swaggerRegex = /\/\*\*[\s\S]*?@swagger[\s\S]*?\*\//g;
|
|
803
|
-
let match;
|
|
804
|
-
|
|
805
|
-
while ((match = swaggerRegex.exec(content)) !== null) {
|
|
806
|
-
swaggerBlocks.push(match[0]);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
return swaggerBlocks;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
/**
|
|
813
|
-
* Extract TypeScript interfaces and types that look like DTOs/schemas
|
|
814
|
-
*/
|
|
815
|
-
function extractTypeDefinitions(content, filePath) {
|
|
816
|
-
const typeBlocks = [];
|
|
817
|
-
const lines = content.split('\n');
|
|
818
|
-
|
|
819
|
-
// Look for interfaces and types that might be request/response schemas
|
|
820
|
-
const typeKeywords = [
|
|
821
|
-
/interface\s+\w*(Request|Response|Dto|Body|Query|Params|Payload|Input|Output)\w*\s*\{/i,
|
|
822
|
-
/type\s+\w*(Request|Response|Dto|Body|Query|Params|Payload|Input|Output)\w*\s*=/i,
|
|
823
|
-
/interface\s+\w+\s*\{/, // Any interface in DTO/schema files
|
|
824
|
-
/type\s+\w+\s*=/ // Any type in DTO/schema files
|
|
825
|
-
];
|
|
826
|
-
|
|
827
|
-
// Only extract type definitions from files that look like DTOs/types
|
|
828
|
-
const isDtoFile = /dto|interface|type|schema|model/i.test(filePath);
|
|
829
|
-
|
|
830
|
-
for (let i = 0; i < lines.length; i++) {
|
|
831
|
-
const line = lines[i];
|
|
832
|
-
|
|
833
|
-
const isTypeDefinition = typeKeywords.some(regex => regex.test(line));
|
|
834
|
-
|
|
835
|
-
if (isTypeDefinition || (isDtoFile && /^(export\s+)?(interface|type)\s+/.test(line))) {
|
|
836
|
-
// Find the complete type block (until closing brace at same indent level)
|
|
837
|
-
let braceCount = 0;
|
|
838
|
-
let started = false;
|
|
839
|
-
let endLine = i;
|
|
840
|
-
|
|
841
|
-
for (let j = i; j < lines.length && j < i + 50; j++) {
|
|
842
|
-
const l = lines[j];
|
|
843
|
-
for (const char of l) {
|
|
844
|
-
if (char === '{') {
|
|
845
|
-
braceCount++;
|
|
846
|
-
started = true;
|
|
847
|
-
} else if (char === '}') {
|
|
848
|
-
braceCount--;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
endLine = j;
|
|
852
|
-
if (started && braceCount === 0) break;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
const block = lines.slice(i, endLine + 1)
|
|
856
|
-
.map((l, idx) => `${i + idx + 1}: ${l}`)
|
|
857
|
-
.join('\n');
|
|
858
|
-
|
|
859
|
-
typeBlocks.push(block);
|
|
860
|
-
i = endLine; // Skip processed lines
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
return typeBlocks;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
/**
|
|
868
|
-
* Extract request body and query parameter patterns
|
|
869
|
-
*/
|
|
870
|
-
function extractRequestPatterns(content) {
|
|
871
|
-
const patterns = [];
|
|
872
|
-
const lines = content.split('\n');
|
|
873
|
-
|
|
874
|
-
for (let i = 0; i < lines.length; i++) {
|
|
875
|
-
const line = lines[i];
|
|
876
|
-
|
|
877
|
-
// Destructuring patterns for req.body, req.query, req.params
|
|
878
|
-
if (/(?:const|let|var)\s*\{[^}]+\}\s*=\s*req\.(body|query|params)/i.test(line) ||
|
|
879
|
-
/req\.(body|query|params)\s*[;.]/.test(line)) {
|
|
880
|
-
|
|
881
|
-
// Get context around it
|
|
882
|
-
const startLine = Math.max(0, i - 2);
|
|
883
|
-
const endLine = Math.min(lines.length - 1, i + 5);
|
|
884
|
-
|
|
885
|
-
const block = lines.slice(startLine, endLine + 1)
|
|
886
|
-
.map((l, idx) => `${startLine + idx + 1}: ${l}`)
|
|
887
|
-
.join('\n');
|
|
888
|
-
|
|
889
|
-
patterns.push(`// Request parameter extraction:\n${block}`);
|
|
890
|
-
i = endLine;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
// NestJS decorators: @Body(), @Query(), @Param()
|
|
894
|
-
if (/@(Body|Query|Param|Headers)\s*\(/i.test(line)) {
|
|
895
|
-
const startLine = Math.max(0, i - 1);
|
|
896
|
-
const endLine = Math.min(lines.length - 1, i + 3);
|
|
897
|
-
|
|
898
|
-
const block = lines.slice(startLine, endLine + 1)
|
|
899
|
-
.map((l, idx) => `${startLine + idx + 1}: ${l}`)
|
|
900
|
-
.join('\n');
|
|
901
|
-
|
|
902
|
-
patterns.push(`// NestJS parameter decorator:\n${block}`);
|
|
903
|
-
i = endLine;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// Validation schemas: Joi, Zod, class-validator
|
|
907
|
-
if (/Joi\.(object|string|number|array|boolean)\s*\(/i.test(line) ||
|
|
908
|
-
/z\.(object|string|number|array|boolean)\s*\(/i.test(line) ||
|
|
909
|
-
/@(IsString|IsNumber|IsArray|IsBoolean|IsOptional|ValidateNested)/i.test(line)) {
|
|
910
|
-
|
|
911
|
-
const startLine = Math.max(0, i - 2);
|
|
912
|
-
const endLine = Math.min(lines.length - 1, i + 10);
|
|
913
|
-
|
|
914
|
-
const block = lines.slice(startLine, endLine + 1)
|
|
915
|
-
.map((l, idx) => `${startLine + idx + 1}: ${l}`)
|
|
916
|
-
.join('\n');
|
|
917
|
-
|
|
918
|
-
patterns.push(`// Validation schema:\n${block}`);
|
|
919
|
-
i = endLine;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
return patterns;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
/**
|
|
927
|
-
* Extract CodeIgniter REST controller methods and map to routes
|
|
928
|
-
* e.g., index_get() -> GET /path, index_post() -> POST /path
|
|
929
|
-
* e.g., items_get() -> GET /path/items, user_get($id) -> GET /path/user/:id
|
|
930
|
-
*/
|
|
931
|
-
function extractCodeIgniterMethods(content, basePath) {
|
|
932
|
-
const routes = [];
|
|
933
|
-
|
|
934
|
-
// Match public function methodname_httpverb($params)
|
|
935
|
-
// CodeIgniter REST convention: {action}_{method}
|
|
936
|
-
const methodRegex = /public\s+function\s+(\w+)_(get|post|put|patch|delete)\s*\(([^)]*)\)/gi;
|
|
937
|
-
let match;
|
|
938
|
-
|
|
939
|
-
while ((match = methodRegex.exec(content)) !== null) {
|
|
940
|
-
const action = match[1]; // e.g., 'index', 'items', 'user'
|
|
941
|
-
const method = match[2].toUpperCase(); // e.g., 'GET', 'POST'
|
|
942
|
-
const params = match[3]; // e.g., '$id = null'
|
|
943
|
-
|
|
944
|
-
// Build the route path
|
|
945
|
-
let path = basePath;
|
|
946
|
-
if (action !== 'index') {
|
|
947
|
-
path += '/' + action.toLowerCase().replace(/_/g, '/');
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// Add path parameters from function arguments
|
|
951
|
-
if (params) {
|
|
952
|
-
const paramNames = params.match(/\$(\w+)/g);
|
|
953
|
-
if (paramNames) {
|
|
954
|
-
paramNames.forEach(p => {
|
|
955
|
-
const paramName = p.replace('$', '');
|
|
956
|
-
if (!paramName.includes('=')) { // Not optional
|
|
957
|
-
path += '/:' + paramName;
|
|
958
|
-
}
|
|
959
|
-
});
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
routes.push(`// ${method} ${path} - from ${match[1]}_${match[2]}()`);
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
return routes;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
/**
|
|
970
|
-
* Extract route path from CodeIgniter/Laravel controller file path
|
|
971
|
-
* e.g., application/controllers/api/v2/Settings.php -> /api/v2/settings
|
|
972
|
-
*/
|
|
973
|
-
function extractRouteFromControllerPath(filePath) {
|
|
974
|
-
// CodeIgniter 3: application/controllers/api/v2/Settings.php -> /api/v2/settings
|
|
975
|
-
let match = filePath.match(/application\/controllers\/(.+)\.php$/i);
|
|
976
|
-
if (match) {
|
|
977
|
-
const route = '/' + match[1].toLowerCase().replace(/\/index$/, '');
|
|
978
|
-
return route;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// CodeIgniter 4: app/Controllers/Api/V2/Settings.php -> /api/v2/settings
|
|
982
|
-
match = filePath.match(/app\/Controllers\/(.+)\.php$/i);
|
|
983
|
-
if (match) {
|
|
984
|
-
const route = '/' + match[1].toLowerCase().replace(/\/index$/, '');
|
|
985
|
-
return route;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// Laravel: app/Http/Controllers/Api/V2/SettingsController.php -> /api/v2/settings
|
|
989
|
-
match = filePath.match(/app\/Http\/Controllers\/(.+)Controller\.php$/i);
|
|
990
|
-
if (match) {
|
|
991
|
-
const route = '/' + match[1].toLowerCase().replace(/\/index$/, '');
|
|
992
|
-
return route;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
return null;
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
/**
|
|
999
|
-
* Extract API patterns from file content - framework agnostic
|
|
1000
|
-
*/
|
|
1001
|
-
function extractApiPatterns(content, filePath, absolutePath) {
|
|
1002
|
-
const lines = content.split('\n');
|
|
1003
|
-
let hasApiPatterns = false;
|
|
1004
|
-
const extractedBlocks = [];
|
|
1005
|
-
|
|
1006
|
-
// Check if this is a route/controller file - if so, include more context
|
|
1007
|
-
const isRouteFile = HIGH_PRIORITY_PATTERNS.some(p => p.test(filePath));
|
|
1008
|
-
// DTO/Model/Entity files across languages
|
|
1009
|
-
const isDtoFile = /dto|interface|types?|schema|model|entity|request|response|form/i.test(filePath);
|
|
1010
|
-
|
|
1011
|
-
// For CodeIgniter/Laravel controllers, extract the route from file path
|
|
1012
|
-
const conventionRoute = extractRouteFromControllerPath(filePath);
|
|
1013
|
-
if (conventionRoute) {
|
|
1014
|
-
// Extract HTTP methods from CodeIgniter-style method names
|
|
1015
|
-
const ciMethods = extractCodeIgniterMethods(content, conventionRoute);
|
|
1016
|
-
if (ciMethods.length > 0) {
|
|
1017
|
-
extractedBlocks.push(`// CONVENTION-BASED ROUTES FROM CONTROLLER:\n${ciMethods.join('\n')}`);
|
|
1018
|
-
hasApiPatterns = true;
|
|
1019
|
-
} else {
|
|
1020
|
-
extractedBlocks.push(`// CONVENTION-BASED ROUTE: ${conventionRoute}\n// Controller file path maps to this API endpoint`);
|
|
1021
|
-
hasApiPatterns = true;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// ============================================
|
|
1026
|
-
// PATTERN 0: Swagger/OpenAPI documentation
|
|
1027
|
-
// ============================================
|
|
1028
|
-
const swaggerBlocks = extractSwaggerBlocks(content);
|
|
1029
|
-
if (swaggerBlocks.length > 0) {
|
|
1030
|
-
hasApiPatterns = true;
|
|
1031
|
-
extractedBlocks.push(`// Swagger/OpenAPI documentation found:\n${swaggerBlocks.join('\n\n')}`);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// ============================================
|
|
1035
|
-
// PATTERN 1: TypeScript interfaces/types (DTOs, schemas)
|
|
1036
|
-
// ============================================
|
|
1037
|
-
const typeBlocks = extractTypeDefinitions(content, filePath);
|
|
1038
|
-
if (typeBlocks.length > 0) {
|
|
1039
|
-
hasApiPatterns = true;
|
|
1040
|
-
extractedBlocks.push(`// TypeScript type definitions:\n${typeBlocks.join('\n\n')}`);
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
// ============================================
|
|
1044
|
-
// PATTERN 2: Request body/query extraction
|
|
1045
|
-
// ============================================
|
|
1046
|
-
const requestPatterns = extractRequestPatterns(content);
|
|
1047
|
-
if (requestPatterns.length > 0) {
|
|
1048
|
-
hasApiPatterns = true;
|
|
1049
|
-
extractedBlocks.push(requestPatterns.join('\n\n'));
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// ============================================
|
|
1053
|
-
// PATTERN 3: Express/Koa/Fastify Router definitions (JavaScript/TypeScript)
|
|
1054
|
-
// ============================================
|
|
1055
|
-
const routerPatterns = [
|
|
1056
|
-
// Express Router: router.get('/path', handler)
|
|
1057
|
-
/\.(get|post|put|patch|delete|all|use)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
1058
|
-
// Express app: app.get('/path', handler)
|
|
1059
|
-
/app\.(get|post|put|patch|delete|all|use)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
1060
|
-
// Fastify: fastify.get('/path', handler)
|
|
1061
|
-
/fastify\.(get|post|put|patch|delete|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi,
|
|
1062
|
-
];
|
|
1063
|
-
|
|
1064
|
-
// ============================================
|
|
1065
|
-
// PATTERN 4: NestJS/Decorators (JavaScript/TypeScript)
|
|
1066
|
-
// ============================================
|
|
1067
|
-
const decoratorPatterns = [
|
|
1068
|
-
/@(Get|Post|Put|Patch|Delete|All)\s*\(\s*['"`]?([^'"`\)]*)/gi,
|
|
1069
|
-
/@Controller\s*\(\s*['"`]([^'"`]+)/gi,
|
|
1070
|
-
];
|
|
1071
|
-
|
|
1072
|
-
// ============================================
|
|
1073
|
-
// PATTERN PHP: CodeIgniter, Laravel, Symfony routes
|
|
1074
|
-
// ============================================
|
|
1075
|
-
const phpPatterns = [
|
|
1076
|
-
// CodeIgniter 4: $routes->get('path', 'Controller::method')
|
|
1077
|
-
/\$routes\s*->\s*(get|post|put|patch|delete|add|match|cli)\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1078
|
-
// CodeIgniter 4: $routes->group()
|
|
1079
|
-
/\$routes\s*->\s*group\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1080
|
-
// CodeIgniter 3: $route['path'] = 'controller/method'
|
|
1081
|
-
/\$route\s*\[\s*['"]([^'"]+)['"]\s*\]/gi,
|
|
1082
|
-
// Laravel: Route::get('path', ...)
|
|
1083
|
-
/Route\s*::\s*(get|post|put|patch|delete|any|match|resource|apiResource)\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1084
|
-
// Laravel route groups
|
|
1085
|
-
/Route\s*::\s*(prefix|group|middleware)\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1086
|
-
// Symfony annotations: @Route("/path")
|
|
1087
|
-
/@Route\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1088
|
-
// Symfony PHP 8 attributes: #[Route('/path')]
|
|
1089
|
-
/#\[Route\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1090
|
-
// PHP method definitions in controllers (for convention-based routing)
|
|
1091
|
-
/public\s+function\s+(\w+)\s*\([^)]*\)/gi,
|
|
1092
|
-
// PHP class definitions (to get controller names)
|
|
1093
|
-
/class\s+(\w+)\s+extends\s+(\w*Controller|CI_Controller|BaseController|ResourceController)/gi,
|
|
1094
|
-
];
|
|
1095
|
-
|
|
1096
|
-
// ============================================
|
|
1097
|
-
// PATTERN Python: Flask, FastAPI, Django
|
|
1098
|
-
// ============================================
|
|
1099
|
-
const pythonPatterns = [
|
|
1100
|
-
// Flask: @app.route('/path')
|
|
1101
|
-
/@app\.(route|get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1102
|
-
// Flask Blueprint: @blueprint.route('/path')
|
|
1103
|
-
/@\w+\.(route|get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1104
|
-
// FastAPI: @app.get('/path'), @router.post('/path')
|
|
1105
|
-
/@(app|router)\.(get|post|put|patch|delete|api_route)\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1106
|
-
// Django: path('route/', view)
|
|
1107
|
-
/path\s*\(\s*['"]([^'"]+)['"]/gi,
|
|
1108
|
-
// Django: url(r'^route/$', view)
|
|
1109
|
-
/url\s*\(\s*r?['"]([^'"]+)['"]/gi,
|
|
1110
|
-
];
|
|
1111
|
-
|
|
1112
|
-
// ============================================
|
|
1113
|
-
// PATTERN Ruby: Rails, Sinatra
|
|
1114
|
-
// ============================================
|
|
1115
|
-
const rubyPatterns = [
|
|
1116
|
-
// Rails: get '/path', post '/path', resources :items
|
|
1117
|
-
/^\s*(get|post|put|patch|delete|resources|resource|root|match)\s+['":]/gim,
|
|
1118
|
-
// Rails namespace/scope
|
|
1119
|
-
/(namespace|scope)\s+[:'"](\w+)/gi,
|
|
1120
|
-
// Sinatra: get '/path' do
|
|
1121
|
-
/^\s*(get|post|put|patch|delete)\s+['"]([^'"]+)['"]\s+do/gim,
|
|
1122
|
-
];
|
|
1123
|
-
|
|
1124
|
-
// ============================================
|
|
1125
|
-
// PATTERN Go: Gin, Echo, Chi, net/http
|
|
1126
|
-
// ============================================
|
|
1127
|
-
const goPatterns = [
|
|
1128
|
-
// Gin: r.GET("/path", handler), router.POST("/path", handler)
|
|
1129
|
-
/\.(GET|POST|PUT|PATCH|DELETE|Handle|Any|Group)\s*\(\s*["']([^"']+)["']/gi,
|
|
1130
|
-
// Echo: e.GET("/path", handler)
|
|
1131
|
-
/e\.(GET|POST|PUT|PATCH|DELETE|Any|Group)\s*\(\s*["']([^"']+)["']/gi,
|
|
1132
|
-
// Chi: r.Get("/path", handler), r.Route("/path", ...)
|
|
1133
|
-
/r\.(Get|Post|Put|Patch|Delete|Route|Group|Mount)\s*\(\s*["']([^"']+)["']/gi,
|
|
1134
|
-
// net/http: http.HandleFunc("/path", handler)
|
|
1135
|
-
/http\.(HandleFunc|Handle)\s*\(\s*["']([^"']+)["']/gi,
|
|
1136
|
-
// Gorilla mux: r.HandleFunc("/path", handler).Methods("GET")
|
|
1137
|
-
/HandleFunc\s*\(\s*["']([^"']+)["']/gi,
|
|
1138
|
-
];
|
|
1139
|
-
|
|
1140
|
-
// ============================================
|
|
1141
|
-
// PATTERN Java/Kotlin: Spring Boot
|
|
1142
|
-
// ============================================
|
|
1143
|
-
const javaPatterns = [
|
|
1144
|
-
// Spring: @GetMapping("/path"), @PostMapping("/path")
|
|
1145
|
-
/@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*\(\s*(?:value\s*=\s*)?["']?([^"'\)]+)/gi,
|
|
1146
|
-
// Spring: @RequestMapping(method = RequestMethod.GET)
|
|
1147
|
-
/@RequestMapping\s*\([^)]*method\s*=\s*RequestMethod\.(GET|POST|PUT|PATCH|DELETE)/gi,
|
|
1148
|
-
// JAX-RS: @GET, @POST, @Path("/path")
|
|
1149
|
-
/@(GET|POST|PUT|PATCH|DELETE|Path)\s*(?:\(\s*["']([^"']+)["']\s*\))?/gi,
|
|
1150
|
-
];
|
|
1151
|
-
|
|
1152
|
-
// ============================================
|
|
1153
|
-
// PATTERN 5: Frontend HTTP calls
|
|
1154
|
-
// ============================================
|
|
1155
|
-
const httpCallPatterns = [
|
|
1156
|
-
// fetch() calls
|
|
1157
|
-
/fetch\s*\(\s*[`'"](.*?)[`'"]/g,
|
|
1158
|
-
/fetch\s*\(\s*`([^`]*)`/g,
|
|
1159
|
-
// axios calls
|
|
1160
|
-
/axios\.(get|post|put|patch|delete)\s*\(\s*[`'"](.*?)[`'"]/g,
|
|
1161
|
-
/axios\s*\(\s*\{[^}]*url\s*:\s*[`'"](.*?)[`'"]/g,
|
|
1162
|
-
// Angular HttpClient
|
|
1163
|
-
/this\.http\.(get|post|put|patch|delete)\s*[<(]/g,
|
|
1164
|
-
/httpClient\.(get|post|put|patch|delete)\s*[<(]/g,
|
|
1165
|
-
// jQuery ajax
|
|
1166
|
-
/\$\.(ajax|get|post)\s*\(\s*[`'"](.*?)[`'"]/g,
|
|
1167
|
-
// Generic request libraries
|
|
1168
|
-
/request\.(get|post|put|patch|delete)\s*\(/g,
|
|
1169
|
-
/got\.(get|post|put|patch|delete)\s*\(/g,
|
|
1170
|
-
/superagent\.(get|post|put|patch|delete)\s*\(/g,
|
|
1171
|
-
];
|
|
1172
|
-
|
|
1173
|
-
// ============================================
|
|
1174
|
-
// PATTERN 6: API URL definitions
|
|
1175
|
-
// ============================================
|
|
1176
|
-
const urlPatterns = [
|
|
1177
|
-
// API endpoints in strings
|
|
1178
|
-
/['"`](\/api\/[^'"`\s]+)['"`]/g,
|
|
1179
|
-
/['"`](\/v\d+\/[^'"`\s]+)['"`]/g,
|
|
1180
|
-
/['"`](https?:\/\/[^'"`\s]*\/api[^'"`\s]*)['"`]/g,
|
|
1181
|
-
// Base URL definitions
|
|
1182
|
-
/(?:API_BASE|API_URL|BASE_URL|baseURL|apiUrl|apiBase|API_ENDPOINT|BACKEND_URL)\s*[:=]\s*['"`]([^'"`]+)['"`]/gi,
|
|
1183
|
-
];
|
|
1184
|
-
|
|
1185
|
-
// ============================================
|
|
1186
|
-
// PATTERN 7: Method + URL combinations
|
|
1187
|
-
// ============================================
|
|
1188
|
-
const methodUrlPatterns = [
|
|
1189
|
-
/(GET|POST|PUT|PATCH|DELETE)\s*[,:]?\s*['"`](\/[^'"`]+)['"`]/gi,
|
|
1190
|
-
/method:\s*['"`](GET|POST|PUT|PATCH|DELETE)['"`]/gi,
|
|
1191
|
-
];
|
|
1192
|
-
|
|
1193
|
-
// Combine all patterns for line scanning (all languages)
|
|
1194
|
-
const allPatterns = [
|
|
1195
|
-
...routerPatterns,
|
|
1196
|
-
...decoratorPatterns,
|
|
1197
|
-
...phpPatterns,
|
|
1198
|
-
...pythonPatterns,
|
|
1199
|
-
...rubyPatterns,
|
|
1200
|
-
...goPatterns,
|
|
1201
|
-
...javaPatterns,
|
|
1202
|
-
...httpCallPatterns,
|
|
1203
|
-
...urlPatterns,
|
|
1204
|
-
...methodUrlPatterns
|
|
1205
|
-
];
|
|
1206
|
-
|
|
1207
|
-
// If this is a route/controller file with swagger docs, include the whole file
|
|
1208
|
-
if (isRouteFile && swaggerBlocks.length > 0) {
|
|
1209
|
-
hasApiPatterns = true;
|
|
1210
|
-
// Include entire file content (truncated if too long)
|
|
1211
|
-
const maxLines = 300;
|
|
1212
|
-
const truncatedContent = lines.length > maxLines
|
|
1213
|
-
? lines.slice(0, maxLines).join('\n') + `\n// ... ${lines.length - maxLines} more lines ...`
|
|
1214
|
-
: content;
|
|
1215
|
-
|
|
1216
|
-
return {
|
|
1217
|
-
hasApiPatterns: true,
|
|
1218
|
-
content: `// File: ${filePath}\n// Route/Controller file with Swagger docs - full content:\n\n${truncatedContent}`
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
// If this is a route file without swagger, still include more content
|
|
1223
|
-
if (isRouteFile) {
|
|
1224
|
-
const hasRouteContent = allPatterns.some(p => {
|
|
1225
|
-
p.lastIndex = 0;
|
|
1226
|
-
return p.test(content);
|
|
1227
|
-
});
|
|
1228
|
-
|
|
1229
|
-
if (hasRouteContent) {
|
|
1230
|
-
hasApiPatterns = true;
|
|
1231
|
-
const maxLines = 200;
|
|
1232
|
-
const truncatedContent = lines.length > maxLines
|
|
1233
|
-
? lines.slice(0, maxLines).join('\n') + `\n// ... ${lines.length - maxLines} more lines ...`
|
|
1234
|
-
: content;
|
|
1235
|
-
|
|
1236
|
-
return {
|
|
1237
|
-
hasApiPatterns: true,
|
|
1238
|
-
content: `// File: ${filePath}\n// Route/Controller file - full content:\n\n${truncatedContent}`
|
|
1239
|
-
};
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
// If this is a DTO/types file, include full content
|
|
1244
|
-
if (isDtoFile && typeBlocks.length > 0) {
|
|
1245
|
-
hasApiPatterns = true;
|
|
1246
|
-
const maxLines = 150;
|
|
1247
|
-
const truncatedContent = lines.length > maxLines
|
|
1248
|
-
? lines.slice(0, maxLines).join('\n') + `\n// ... ${lines.length - maxLines} more lines ...`
|
|
1249
|
-
: content;
|
|
1250
|
-
|
|
1251
|
-
return {
|
|
1252
|
-
hasApiPatterns: true,
|
|
1253
|
-
content: `// File: ${filePath}\n// DTO/Types file - full content:\n\n${truncatedContent}`
|
|
1254
|
-
};
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
// For non-route files, extract relevant sections
|
|
1258
|
-
// First, extract imports and base URL definitions
|
|
1259
|
-
const baseUrlLines = [];
|
|
1260
|
-
|
|
1261
|
-
for (let i = 0; i < Math.min(lines.length, 50); i++) {
|
|
1262
|
-
const line = lines[i];
|
|
1263
|
-
if (/(?:API_BASE|API_URL|BASE_URL|baseURL|apiUrl|BACKEND)/i.test(line)) {
|
|
1264
|
-
baseUrlLines.push(`${i + 1}: ${line}`);
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
if (baseUrlLines.length > 0) {
|
|
1269
|
-
extractedBlocks.push('// Base URL definitions:\n' + baseUrlLines.join('\n'));
|
|
1270
|
-
hasApiPatterns = true;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
// Line-by-line extraction with context
|
|
1274
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1275
|
-
const line = lines[i];
|
|
1276
|
-
|
|
1277
|
-
// Check all patterns
|
|
1278
|
-
let hasPattern = false;
|
|
1279
|
-
for (const pattern of allPatterns) {
|
|
1280
|
-
pattern.lastIndex = 0;
|
|
1281
|
-
if (pattern.test(line)) {
|
|
1282
|
-
hasPattern = true;
|
|
1283
|
-
break;
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// Also check for common API keywords (all languages)
|
|
1288
|
-
const hasKeyword = new RegExp([
|
|
1289
|
-
// JavaScript/TypeScript
|
|
1290
|
-
'\\.get\\(', '\\.post\\(', '\\.put\\(', '\\.patch\\(', '\\.delete\\(',
|
|
1291
|
-
'fetch\\(', 'axios', '\\/api\\/', 'endpoint',
|
|
1292
|
-
'@Get', '@Post', '@Put', '@Delete',
|
|
1293
|
-
// PHP (CodeIgniter, Laravel)
|
|
1294
|
-
'\\$routes\\s*->', 'Route\\s*::', '@Route',
|
|
1295
|
-
'\\$this->input->', '\\$this->request->', // CodeIgniter input
|
|
1296
|
-
'\\$request->', '\\$_POST', '\\$_GET', '\\$_PUT', // PHP request data
|
|
1297
|
-
'public\\s+function\\s+\\w+', // PHP public methods (potential endpoints)
|
|
1298
|
-
'extends\\s+\\w*Controller', // PHP controller classes
|
|
1299
|
-
// Python (Flask, FastAPI, Django)
|
|
1300
|
-
'@app\\.route', '@app\\.get', '@app\\.post', '@router\\.',
|
|
1301
|
-
'path\\s*\\(', 'url\\s*\\(',
|
|
1302
|
-
// Ruby (Rails)
|
|
1303
|
-
'resources\\s+:', 'get\\s+[\'"]/', 'post\\s+[\'"]/',
|
|
1304
|
-
// Go (Gin, Echo, Chi)
|
|
1305
|
-
'\\.GET\\(', '\\.POST\\(', 'HandleFunc\\(',
|
|
1306
|
-
// Java/Kotlin (Spring)
|
|
1307
|
-
'@GetMapping', '@PostMapping', '@RequestMapping', '@Path'
|
|
1308
|
-
].join('|'), 'i').test(line);
|
|
1309
|
-
|
|
1310
|
-
if (hasPattern || hasKeyword) {
|
|
1311
|
-
hasApiPatterns = true;
|
|
1312
|
-
|
|
1313
|
-
// Determine context needed
|
|
1314
|
-
const isPostOrPut = /post|put|patch/i.test(line);
|
|
1315
|
-
const contextBefore = isPostOrPut ? 20 : 5; // More context for mutations
|
|
1316
|
-
const contextAfter = isPostOrPut ? 10 : 5;
|
|
1317
|
-
|
|
1318
|
-
const startLine = Math.max(0, i - contextBefore);
|
|
1319
|
-
const endLine = Math.min(lines.length - 1, i + contextAfter);
|
|
1320
|
-
|
|
1321
|
-
const block = lines.slice(startLine, endLine + 1)
|
|
1322
|
-
.map((l, idx) => `${startLine + idx + 1}: ${l}`)
|
|
1323
|
-
.join('\n');
|
|
1324
|
-
|
|
1325
|
-
extractedBlocks.push(block);
|
|
1326
|
-
|
|
1327
|
-
// Skip ahead to avoid duplicates
|
|
1328
|
-
i = endLine;
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
if (hasApiPatterns && extractedBlocks.length > 0) {
|
|
1333
|
-
// Deduplicate blocks
|
|
1334
|
-
const uniqueBlocks = [...new Set(extractedBlocks)];
|
|
1335
|
-
return {
|
|
1336
|
-
hasApiPatterns: true,
|
|
1337
|
-
content: `// File: ${filePath}\n// API patterns extracted:\n\n${uniqueBlocks.join('\n\n// ---\n\n')}`
|
|
1338
|
-
};
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
return { hasApiPatterns: false, content: '' };
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
module.exports = { scanCodebase };
|