@merlean/analyzer 2.2.0 → 2.3.0
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/bin/cli.js +47 -12
- package/lib/analyzer.js +858 -49
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -23,7 +23,8 @@ function parseArgs() {
|
|
|
23
23
|
path: null,
|
|
24
24
|
name: null,
|
|
25
25
|
backend: DEFAULT_BACKEND,
|
|
26
|
-
output: null
|
|
26
|
+
output: null,
|
|
27
|
+
mergeWith: null // Existing siteId to merge into
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -35,6 +36,8 @@ function parseArgs() {
|
|
|
35
36
|
options.backend = args[++i];
|
|
36
37
|
} else if (arg === '--output' || arg === '-o') {
|
|
37
38
|
options.output = args[++i];
|
|
39
|
+
} else if (arg === '--merge-with' || arg === '-m') {
|
|
40
|
+
options.mergeWith = args[++i];
|
|
38
41
|
} else if (arg === '--help' || arg === '-h') {
|
|
39
42
|
printHelp();
|
|
40
43
|
process.exit(0);
|
|
@@ -58,7 +61,8 @@ Arguments:
|
|
|
58
61
|
<path> Path to codebase (default: current directory)
|
|
59
62
|
|
|
60
63
|
Options:
|
|
61
|
-
--name, -n <name> Site name (required)
|
|
64
|
+
--name, -n <name> Site name (required for new analysis)
|
|
65
|
+
--merge-with, -m <siteId> Merge into existing site map (appends routes/forms/actions)
|
|
62
66
|
--backend, -b <url> Backend URL (default: ${DEFAULT_BACKEND})
|
|
63
67
|
--output, -o <file> Save site map locally (optional)
|
|
64
68
|
--help, -h Show this help
|
|
@@ -70,6 +74,9 @@ Examples:
|
|
|
70
74
|
# Analyze specific path
|
|
71
75
|
npx @merlean/analyzer ./my-app --name "My App"
|
|
72
76
|
|
|
77
|
+
# Merge another frontend into existing site map
|
|
78
|
+
npx @merlean/analyzer ./admin-panel --merge-with site_abc123
|
|
79
|
+
|
|
73
80
|
# Use custom backend (for local dev)
|
|
74
81
|
npx @merlean/analyzer --name "My App" --backend http://localhost:3004
|
|
75
82
|
`);
|
|
@@ -81,8 +88,8 @@ async function main() {
|
|
|
81
88
|
const options = parseArgs();
|
|
82
89
|
|
|
83
90
|
// Validate required args
|
|
84
|
-
if (!options.name) {
|
|
85
|
-
console.error('❌ Error: --name is required');
|
|
91
|
+
if (!options.name && !options.mergeWith) {
|
|
92
|
+
console.error('❌ Error: --name is required (or use --merge-with to merge into existing site)');
|
|
86
93
|
console.log(' Run with --help for usage');
|
|
87
94
|
process.exit(1);
|
|
88
95
|
}
|
|
@@ -96,22 +103,41 @@ async function main() {
|
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
console.log(`📁 Scanning: ${codebasePath}`);
|
|
99
|
-
|
|
106
|
+
if (options.mergeWith) {
|
|
107
|
+
console.log(`🔗 Merging into: ${options.mergeWith}`);
|
|
108
|
+
if (options.name) {
|
|
109
|
+
console.log(`📛 Site name: ${options.name} (will update existing)`);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
console.log(`📛 Site name: ${options.name}`);
|
|
113
|
+
}
|
|
100
114
|
|
|
101
115
|
try {
|
|
102
116
|
// Scan codebase locally
|
|
103
|
-
const
|
|
117
|
+
const scanResult = await scanCodebase(codebasePath);
|
|
118
|
+
// Handle both old format (array) and new format (object with files + preExtractedRoutes)
|
|
119
|
+
const fileContents = Array.isArray(scanResult) ? scanResult : scanResult.files;
|
|
120
|
+
const preExtractedRoutes = Array.isArray(scanResult) ? [] : (scanResult.preExtractedRoutes || []);
|
|
104
121
|
|
|
105
122
|
console.log(`\n📤 Uploading to backend for analysis...`);
|
|
106
123
|
|
|
124
|
+
// Build request body
|
|
125
|
+
const requestBody = {
|
|
126
|
+
siteName: options.name,
|
|
127
|
+
files: fileContents,
|
|
128
|
+
preExtractedRoutes // Include convention-based routes
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Add merge parameter if specified
|
|
132
|
+
if (options.mergeWith) {
|
|
133
|
+
requestBody.mergeWithSiteId = options.mergeWith;
|
|
134
|
+
}
|
|
135
|
+
|
|
107
136
|
// Send to backend for LLM analysis
|
|
108
137
|
const response = await fetch(`${options.backend}/api/analyze`, {
|
|
109
138
|
method: 'POST',
|
|
110
139
|
headers: { 'Content-Type': 'application/json' },
|
|
111
|
-
body: JSON.stringify(
|
|
112
|
-
siteName: options.name,
|
|
113
|
-
files: fileContents
|
|
114
|
-
})
|
|
140
|
+
body: JSON.stringify(requestBody)
|
|
115
141
|
});
|
|
116
142
|
|
|
117
143
|
if (!response.ok) {
|
|
@@ -121,13 +147,22 @@ async function main() {
|
|
|
121
147
|
|
|
122
148
|
const result = await response.json();
|
|
123
149
|
|
|
124
|
-
|
|
125
|
-
|
|
150
|
+
if (options.mergeWith) {
|
|
151
|
+
console.log('\n✅ Merge complete!');
|
|
152
|
+
console.log('\n📊 Updated Summary:');
|
|
153
|
+
} else {
|
|
154
|
+
console.log('\n✅ Analysis complete!');
|
|
155
|
+
console.log('\n📊 Summary:');
|
|
156
|
+
}
|
|
126
157
|
console.log(` Site ID: ${result.siteId}`);
|
|
127
158
|
console.log(` Framework: ${result.framework || 'Unknown'}`);
|
|
128
159
|
console.log(` Routes: ${result.routes?.length || 0}`);
|
|
129
160
|
console.log(` Forms: ${result.forms?.length || 0}`);
|
|
130
161
|
console.log(` Actions: ${result.actions?.length || 0}`);
|
|
162
|
+
|
|
163
|
+
if (result.merged) {
|
|
164
|
+
console.log(`\n 📎 Merged from: ${result.merged.sourcesCount} source(s)`);
|
|
165
|
+
}
|
|
131
166
|
|
|
132
167
|
// Save locally if requested
|
|
133
168
|
if (options.output) {
|
package/lib/analyzer.js
CHANGED
|
@@ -1,25 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Framework
|
|
2
|
+
* Language & Framework Agnostic Codebase Scanner
|
|
3
3
|
*
|
|
4
|
-
* Scans ANY codebase (frontend or backend) to extract API patterns:
|
|
4
|
+
* Scans ANY codebase (frontend or backend, any language) to extract API patterns:
|
|
5
|
+
*
|
|
6
|
+
* JavaScript/TypeScript:
|
|
5
7
|
* - Express/Fastify/Koa/Hapi route definitions
|
|
6
8
|
* - NestJS decorators (@Get, @Post, @Body, @Query, etc.)
|
|
7
9
|
* - Frontend HTTP calls (fetch, axios, HttpClient, etc.)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
11
|
-
* -
|
|
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
|
|
12
34
|
*
|
|
13
35
|
* The goal is to understand what APIs exist, their parameters,
|
|
14
|
-
* and body structures - regardless of framework or code style.
|
|
36
|
+
* and body structures - regardless of framework, language, or code style.
|
|
15
37
|
*/
|
|
16
38
|
|
|
17
39
|
const fs = require('fs');
|
|
18
40
|
const path = require('path');
|
|
19
41
|
const { glob } = require('glob');
|
|
20
42
|
|
|
21
|
-
// File patterns to scan
|
|
43
|
+
// File patterns to scan - include ALL common backend/frontend languages
|
|
22
44
|
const FILE_PATTERNS = [
|
|
45
|
+
// JavaScript/TypeScript
|
|
23
46
|
'**/*.js',
|
|
24
47
|
'**/*.jsx',
|
|
25
48
|
'**/*.ts',
|
|
@@ -27,7 +50,22 @@ const FILE_PATTERNS = [
|
|
|
27
50
|
'**/*.vue',
|
|
28
51
|
'**/*.svelte',
|
|
29
52
|
'**/*.mjs',
|
|
30
|
-
'**/*.cjs'
|
|
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'
|
|
31
69
|
];
|
|
32
70
|
|
|
33
71
|
// Only ignore truly irrelevant directories
|
|
@@ -51,47 +89,74 @@ const IGNORE_PATTERNS = [
|
|
|
51
89
|
'**/__mocks__/**'
|
|
52
90
|
];
|
|
53
91
|
|
|
54
|
-
// Files that are highly likely to contain API definitions
|
|
92
|
+
// Files that are highly likely to contain API definitions (any language)
|
|
55
93
|
const HIGH_PRIORITY_PATTERNS = [
|
|
56
|
-
/
|
|
57
|
-
/routes?\.js$/i,
|
|
58
|
-
/router\.ts$/i,
|
|
59
|
-
/
|
|
60
|
-
|
|
61
|
-
/
|
|
62
|
-
|
|
63
|
-
/\.
|
|
64
|
-
/
|
|
65
|
-
/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/
|
|
70
|
-
/endpoints?\.ts$/i,
|
|
71
|
-
/endpoints?\.js$/i,
|
|
72
|
-
/http\.ts$/i,
|
|
73
|
-
/http\.js$/i,
|
|
74
|
-
/client\.ts$/i,
|
|
75
|
-
/client\.js$/i,
|
|
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
|
|
76
108
|
/dto\.ts$/i,
|
|
77
109
|
/\.dto\.ts$/i,
|
|
78
110
|
/interfaces?\.ts$/i,
|
|
79
111
|
/types?\.ts$/i,
|
|
80
112
|
/schema\.ts$/i,
|
|
81
113
|
/schemas?\.ts$/i,
|
|
82
|
-
/validation\.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$/
|
|
83
149
|
];
|
|
84
150
|
|
|
85
151
|
// Medium priority - might contain API patterns
|
|
86
152
|
const MEDIUM_PRIORITY_PATTERNS = [
|
|
87
|
-
/index\.ts$/i,
|
|
88
|
-
/
|
|
89
|
-
/
|
|
90
|
-
/
|
|
91
|
-
/main
|
|
92
|
-
/
|
|
93
|
-
/
|
|
94
|
-
/server\.js$/i
|
|
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
|
|
95
160
|
];
|
|
96
161
|
|
|
97
162
|
/**
|
|
@@ -116,15 +181,32 @@ async function scanCodebase(codebasePath) {
|
|
|
116
181
|
|
|
117
182
|
// Analyze high priority files first (routes, controllers, services, DTOs)
|
|
118
183
|
// Then medium priority, then scan others for API patterns
|
|
184
|
+
// Increased limits to ensure all controllers are scanned
|
|
119
185
|
const filesToAnalyze = [
|
|
120
|
-
...highPriority,
|
|
121
|
-
...mediumPriority.slice(0,
|
|
122
|
-
...other.slice(0,
|
|
186
|
+
...highPriority, // ALL high priority files (controllers, routes, etc.)
|
|
187
|
+
...mediumPriority.slice(0, 30),
|
|
188
|
+
...other.slice(0, 75)
|
|
123
189
|
];
|
|
124
190
|
|
|
125
191
|
console.log(` Analyzing ${filesToAnalyze.length} files for API patterns...`);
|
|
126
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
|
|
127
208
|
const fileContents = [];
|
|
209
|
+
const preExtractedRoutes = []; // Routes extracted directly from conventions
|
|
128
210
|
let filesWithPatterns = 0;
|
|
129
211
|
|
|
130
212
|
for (const file of filesToAnalyze) {
|
|
@@ -132,6 +214,12 @@ async function scanCodebase(codebasePath) {
|
|
|
132
214
|
const content = fs.readFileSync(file, 'utf-8');
|
|
133
215
|
const relativePath = path.relative(codebasePath, file);
|
|
134
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
|
+
|
|
135
223
|
// Extract API patterns from the file
|
|
136
224
|
const extracted = extractApiPatterns(content, relativePath, file);
|
|
137
225
|
|
|
@@ -148,8 +236,537 @@ async function scanCodebase(codebasePath) {
|
|
|
148
236
|
}
|
|
149
237
|
|
|
150
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
|
+
}
|
|
151
242
|
|
|
152
|
-
return fileContents;
|
|
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';
|
|
153
770
|
}
|
|
154
771
|
|
|
155
772
|
/**
|
|
@@ -306,6 +923,78 @@ function extractRequestPatterns(content) {
|
|
|
306
923
|
return patterns;
|
|
307
924
|
}
|
|
308
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
|
+
|
|
309
998
|
/**
|
|
310
999
|
* Extract API patterns from file content - framework agnostic
|
|
311
1000
|
*/
|
|
@@ -316,7 +1005,22 @@ function extractApiPatterns(content, filePath, absolutePath) {
|
|
|
316
1005
|
|
|
317
1006
|
// Check if this is a route/controller file - if so, include more context
|
|
318
1007
|
const isRouteFile = HIGH_PRIORITY_PATTERNS.some(p => p.test(filePath));
|
|
319
|
-
|
|
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
|
+
}
|
|
320
1024
|
|
|
321
1025
|
// ============================================
|
|
322
1026
|
// PATTERN 0: Swagger/OpenAPI documentation
|
|
@@ -346,7 +1050,7 @@ function extractApiPatterns(content, filePath, absolutePath) {
|
|
|
346
1050
|
}
|
|
347
1051
|
|
|
348
1052
|
// ============================================
|
|
349
|
-
// PATTERN 3: Express/Koa/Fastify Router definitions
|
|
1053
|
+
// PATTERN 3: Express/Koa/Fastify Router definitions (JavaScript/TypeScript)
|
|
350
1054
|
// ============================================
|
|
351
1055
|
const routerPatterns = [
|
|
352
1056
|
// Express Router: router.get('/path', handler)
|
|
@@ -358,12 +1062,92 @@ function extractApiPatterns(content, filePath, absolutePath) {
|
|
|
358
1062
|
];
|
|
359
1063
|
|
|
360
1064
|
// ============================================
|
|
361
|
-
// PATTERN 4: NestJS/Decorators
|
|
1065
|
+
// PATTERN 4: NestJS/Decorators (JavaScript/TypeScript)
|
|
362
1066
|
// ============================================
|
|
363
1067
|
const decoratorPatterns = [
|
|
364
1068
|
/@(Get|Post|Put|Patch|Delete|All)\s*\(\s*['"`]?([^'"`\)]*)/gi,
|
|
365
1069
|
/@Controller\s*\(\s*['"`]([^'"`]+)/gi,
|
|
366
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
|
+
];
|
|
367
1151
|
|
|
368
1152
|
// ============================================
|
|
369
1153
|
// PATTERN 5: Frontend HTTP calls
|
|
@@ -406,10 +1190,15 @@ function extractApiPatterns(content, filePath, absolutePath) {
|
|
|
406
1190
|
/method:\s*['"`](GET|POST|PUT|PATCH|DELETE)['"`]/gi,
|
|
407
1191
|
];
|
|
408
1192
|
|
|
409
|
-
// Combine all patterns for line scanning
|
|
1193
|
+
// Combine all patterns for line scanning (all languages)
|
|
410
1194
|
const allPatterns = [
|
|
411
1195
|
...routerPatterns,
|
|
412
1196
|
...decoratorPatterns,
|
|
1197
|
+
...phpPatterns,
|
|
1198
|
+
...pythonPatterns,
|
|
1199
|
+
...rubyPatterns,
|
|
1200
|
+
...goPatterns,
|
|
1201
|
+
...javaPatterns,
|
|
413
1202
|
...httpCallPatterns,
|
|
414
1203
|
...urlPatterns,
|
|
415
1204
|
...methodUrlPatterns
|
|
@@ -495,8 +1284,28 @@ function extractApiPatterns(content, filePath, absolutePath) {
|
|
|
495
1284
|
}
|
|
496
1285
|
}
|
|
497
1286
|
|
|
498
|
-
// Also check for common API keywords
|
|
499
|
-
const hasKeyword =
|
|
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);
|
|
500
1309
|
|
|
501
1310
|
if (hasPattern || hasKeyword) {
|
|
502
1311
|
hasApiPatterns = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@merlean/analyzer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "AI Bot codebase analyzer - generates site maps for AI assistant integration",
|
|
5
5
|
"keywords": ["ai", "bot", "analyzer", "claude", "anthropic", "widget"],
|
|
6
6
|
"author": "zmaren",
|