docguard-cli 0.5.1 → 0.6.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/README.md +37 -15
- package/cli/commands/generate.mjs +432 -82
- package/cli/commands/publish.mjs +246 -0
- package/cli/docguard.mjs +19 -3
- package/cli/scanners/doc-tools.mjs +351 -0
- package/cli/scanners/routes.mjs +461 -0
- package/cli/scanners/schemas.mjs +567 -0
- package/docs/ai-integration.md +7 -7
- package/docs/commands.md +40 -40
- package/docs/faq.md +1 -1
- package/docs/profiles.md +6 -6
- package/docs/quickstart.md +10 -10
- package/package.json +1 -1
- package/templates/ci/github-actions.yml +1 -1
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep Route Scanner
|
|
3
|
+
* Parses actual route definitions from source code across frameworks.
|
|
4
|
+
* Supports: Next.js (App Router + Pages), Express, Fastify, Hono, Django, FastAPI
|
|
5
|
+
*
|
|
6
|
+
* Priority: OpenAPI spec > Code scanning
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
10
|
+
import { resolve, join, relative, basename, extname, dirname } from 'node:path';
|
|
11
|
+
|
|
12
|
+
const IGNORE_DIRS = new Set([
|
|
13
|
+
'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
|
|
14
|
+
'.cache', '__pycache__', '.venv', 'vendor', '.turbo',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Scan routes from source code with framework-aware parsing.
|
|
19
|
+
* @param {string} dir - Project root
|
|
20
|
+
* @param {object} stack - Detected tech stack
|
|
21
|
+
* @param {object} docTools - Detected doc tools (may include OpenAPI)
|
|
22
|
+
* @returns {Array} Array of route objects { method, path, handler, file, auth, description }
|
|
23
|
+
*/
|
|
24
|
+
export function scanRoutesDeep(dir, stack, docTools) {
|
|
25
|
+
// Priority 1: Use OpenAPI spec if available (most accurate)
|
|
26
|
+
if (docTools?.openapi?.found && docTools.openapi.endpoints?.length > 0) {
|
|
27
|
+
return docTools.openapi.endpoints.map(ep => ({
|
|
28
|
+
...ep,
|
|
29
|
+
source: 'openapi',
|
|
30
|
+
file: docTools.openapi.path,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Priority 2: Framework-specific code scanning
|
|
35
|
+
const framework = stack?.framework || '';
|
|
36
|
+
const routes = [];
|
|
37
|
+
|
|
38
|
+
if (framework.includes('Next.js') || framework.includes('Next')) {
|
|
39
|
+
routes.push(...scanNextJsRoutes(dir));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (framework.includes('Express') || !framework) {
|
|
43
|
+
routes.push(...scanExpressRoutes(dir));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (framework.includes('Fastify')) {
|
|
47
|
+
routes.push(...scanFastifyRoutes(dir));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (framework.includes('Hono')) {
|
|
51
|
+
routes.push(...scanHonoRoutes(dir));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (framework.includes('Django')) {
|
|
55
|
+
routes.push(...scanDjangoRoutes(dir));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (framework.includes('FastAPI') || framework.includes('Flask')) {
|
|
59
|
+
routes.push(...scanFastAPIRoutes(dir));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Deduplicate by method+path
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
return routes.filter(r => {
|
|
65
|
+
const key = `${r.method}:${r.path}`;
|
|
66
|
+
if (seen.has(key)) return false;
|
|
67
|
+
seen.add(key);
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Next.js App Router ──────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
function scanNextJsRoutes(dir) {
|
|
75
|
+
const routes = [];
|
|
76
|
+
|
|
77
|
+
// App Router: app/api/**/route.{ts,js}
|
|
78
|
+
const appDirs = ['app/api', 'src/app/api'];
|
|
79
|
+
for (const appDir of appDirs) {
|
|
80
|
+
const fullDir = resolve(dir, appDir);
|
|
81
|
+
if (!existsSync(fullDir)) continue;
|
|
82
|
+
|
|
83
|
+
walkRouteDirs(fullDir, (filePath) => {
|
|
84
|
+
const name = basename(filePath);
|
|
85
|
+
if (!/^route\.(ts|tsx|js|jsx|mjs)$/.test(name)) return;
|
|
86
|
+
|
|
87
|
+
const content = readFileSafe(filePath);
|
|
88
|
+
if (!content) return;
|
|
89
|
+
|
|
90
|
+
// Path from directory structure
|
|
91
|
+
const relDir = relative(resolve(dir, appDir.split('/')[0]), dirname(filePath));
|
|
92
|
+
const apiPath = '/' + relDir
|
|
93
|
+
.replace(/\\/g, '/')
|
|
94
|
+
.replace(/\[\.\.\.(\w+)\]/g, ':$1*') // Catch-all [...slug]
|
|
95
|
+
.replace(/\[(\w+)\]/g, ':$1'); // Dynamic [id]
|
|
96
|
+
|
|
97
|
+
// Extract exported HTTP methods
|
|
98
|
+
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
|
99
|
+
for (const method of methods) {
|
|
100
|
+
// Match: export async function GET, export function GET, export const GET
|
|
101
|
+
const patterns = [
|
|
102
|
+
new RegExp(`export\\s+(?:async\\s+)?function\\s+${method}\\b`),
|
|
103
|
+
new RegExp(`export\\s+(?:const|let)\\s+${method}\\s*=`),
|
|
104
|
+
];
|
|
105
|
+
for (const pattern of patterns) {
|
|
106
|
+
if (pattern.test(content)) {
|
|
107
|
+
routes.push({
|
|
108
|
+
method,
|
|
109
|
+
path: apiPath,
|
|
110
|
+
handler: method,
|
|
111
|
+
file: relative(dir, filePath),
|
|
112
|
+
source: 'nextjs-app-router',
|
|
113
|
+
auth: hasAuthCheck(content),
|
|
114
|
+
description: extractJSDocDescription(content, method),
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Pages Router: pages/api/**/*.{ts,js}
|
|
124
|
+
const pagesDirs = ['pages/api', 'src/pages/api'];
|
|
125
|
+
for (const pagesDir of pagesDirs) {
|
|
126
|
+
const fullDir = resolve(dir, pagesDir);
|
|
127
|
+
if (!existsSync(fullDir)) continue;
|
|
128
|
+
|
|
129
|
+
walkRouteDirs(fullDir, (filePath) => {
|
|
130
|
+
const ext = extname(filePath);
|
|
131
|
+
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs'].includes(ext)) return;
|
|
132
|
+
const name = basename(filePath, ext);
|
|
133
|
+
if (name.startsWith('_')) return; // Skip _middleware, _document, etc.
|
|
134
|
+
|
|
135
|
+
const content = readFileSafe(filePath);
|
|
136
|
+
if (!content) return;
|
|
137
|
+
|
|
138
|
+
// Path from file structure
|
|
139
|
+
const relPath = relative(fullDir, filePath);
|
|
140
|
+
const apiPath = '/api/' + relPath
|
|
141
|
+
.replace(/\\/g, '/')
|
|
142
|
+
.replace(extname(relPath), '')
|
|
143
|
+
.replace(/index$/, '')
|
|
144
|
+
.replace(/\[\.\.\.(\w+)\]/g, ':$1*')
|
|
145
|
+
.replace(/\[(\w+)\]/g, ':$1')
|
|
146
|
+
.replace(/\/$/, '');
|
|
147
|
+
|
|
148
|
+
// Detect methods from req.method checks
|
|
149
|
+
const detectedMethods = detectMethodsFromHandler(content);
|
|
150
|
+
|
|
151
|
+
for (const method of detectedMethods) {
|
|
152
|
+
routes.push({
|
|
153
|
+
method,
|
|
154
|
+
path: apiPath || '/api',
|
|
155
|
+
handler: `${name}Handler`,
|
|
156
|
+
file: relative(dir, filePath),
|
|
157
|
+
source: 'nextjs-pages-router',
|
|
158
|
+
auth: hasAuthCheck(content),
|
|
159
|
+
description: extractJSDocDescription(content),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return routes;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Express / Generic Node.js ───────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
function scanExpressRoutes(dir) {
|
|
171
|
+
const routes = [];
|
|
172
|
+
const routePattern = /(?:app|router|server)\s*\.\s*(get|post|put|delete|patch|head|options)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
173
|
+
|
|
174
|
+
const searchDirs = ['src', 'routes', 'api', 'server', 'lib'];
|
|
175
|
+
for (const searchDir of searchDirs) {
|
|
176
|
+
const fullDir = resolve(dir, searchDir);
|
|
177
|
+
if (!existsSync(fullDir)) continue;
|
|
178
|
+
|
|
179
|
+
walkRouteDirs(fullDir, (filePath) => {
|
|
180
|
+
if (!isJSFile(filePath)) return;
|
|
181
|
+
const content = readFileSafe(filePath);
|
|
182
|
+
if (!content) return;
|
|
183
|
+
|
|
184
|
+
let match;
|
|
185
|
+
const regex = new RegExp(routePattern.source, 'gi');
|
|
186
|
+
while ((match = regex.exec(content)) !== null) {
|
|
187
|
+
routes.push({
|
|
188
|
+
method: match[1].toUpperCase(),
|
|
189
|
+
path: match[2],
|
|
190
|
+
handler: extractHandlerName(content, match.index),
|
|
191
|
+
file: relative(dir, filePath),
|
|
192
|
+
source: 'express',
|
|
193
|
+
auth: hasAuthMiddleware(content, match[2]),
|
|
194
|
+
description: extractNearbyComment(content, match.index),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Also check root files
|
|
201
|
+
for (const rootFile of ['app.js', 'app.mjs', 'app.ts', 'server.js', 'server.ts', 'index.js', 'index.ts']) {
|
|
202
|
+
const filePath = resolve(dir, rootFile);
|
|
203
|
+
if (!existsSync(filePath)) continue;
|
|
204
|
+
const content = readFileSafe(filePath);
|
|
205
|
+
if (!content) return;
|
|
206
|
+
|
|
207
|
+
let match;
|
|
208
|
+
const regex = new RegExp(routePattern.source, 'gi');
|
|
209
|
+
while ((match = regex.exec(content)) !== null) {
|
|
210
|
+
routes.push({
|
|
211
|
+
method: match[1].toUpperCase(),
|
|
212
|
+
path: match[2],
|
|
213
|
+
handler: extractHandlerName(content, match.index),
|
|
214
|
+
file: rootFile,
|
|
215
|
+
source: 'express',
|
|
216
|
+
auth: hasAuthMiddleware(content, match[2]),
|
|
217
|
+
description: extractNearbyComment(content, match.index),
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return routes;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Fastify ─────────────────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
function scanFastifyRoutes(dir) {
|
|
228
|
+
const routes = [];
|
|
229
|
+
const pattern = /(?:fastify|server|app)\s*\.\s*(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
230
|
+
|
|
231
|
+
walkRouteDirs(resolve(dir, 'src'), (filePath) => {
|
|
232
|
+
if (!isJSFile(filePath)) return;
|
|
233
|
+
const content = readFileSafe(filePath);
|
|
234
|
+
if (!content) return;
|
|
235
|
+
|
|
236
|
+
let match;
|
|
237
|
+
const regex = new RegExp(pattern.source, 'gi');
|
|
238
|
+
while ((match = regex.exec(content)) !== null) {
|
|
239
|
+
routes.push({
|
|
240
|
+
method: match[1].toUpperCase(),
|
|
241
|
+
path: match[2],
|
|
242
|
+
handler: extractHandlerName(content, match.index),
|
|
243
|
+
file: relative(dir, filePath),
|
|
244
|
+
source: 'fastify',
|
|
245
|
+
auth: hasAuthCheck(content),
|
|
246
|
+
description: extractNearbyComment(content, match.index),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return routes;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── Hono ────────────────────────────────────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
function scanHonoRoutes(dir) {
|
|
257
|
+
const routes = [];
|
|
258
|
+
const pattern = /(?:app|router)\s*\.\s*(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
259
|
+
|
|
260
|
+
const searchDirs = ['src', '.'];
|
|
261
|
+
for (const searchDir of searchDirs) {
|
|
262
|
+
const fullDir = resolve(dir, searchDir);
|
|
263
|
+
if (!existsSync(fullDir)) continue;
|
|
264
|
+
|
|
265
|
+
walkRouteDirs(fullDir, (filePath) => {
|
|
266
|
+
if (!isJSFile(filePath)) return;
|
|
267
|
+
const content = readFileSafe(filePath);
|
|
268
|
+
if (!content) return;
|
|
269
|
+
|
|
270
|
+
let match;
|
|
271
|
+
const regex = new RegExp(pattern.source, 'gi');
|
|
272
|
+
while ((match = regex.exec(content)) !== null) {
|
|
273
|
+
routes.push({
|
|
274
|
+
method: match[1].toUpperCase(),
|
|
275
|
+
path: match[2],
|
|
276
|
+
handler: '',
|
|
277
|
+
file: relative(dir, filePath),
|
|
278
|
+
source: 'hono',
|
|
279
|
+
auth: hasAuthCheck(content),
|
|
280
|
+
description: extractNearbyComment(content, match.index),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return routes;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── Django ───────────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
function scanDjangoRoutes(dir) {
|
|
292
|
+
const routes = [];
|
|
293
|
+
const urlsFiles = findFiles(dir, /urls\.py$/);
|
|
294
|
+
|
|
295
|
+
for (const filePath of urlsFiles) {
|
|
296
|
+
const content = readFileSafe(filePath);
|
|
297
|
+
if (!content) continue;
|
|
298
|
+
|
|
299
|
+
// Match: path('api/users/', views.user_list, name='user-list')
|
|
300
|
+
const pathPattern = /path\s*\(\s*['"]([^'"]+)['"]\s*,\s*(\w+[\w.]*)/g;
|
|
301
|
+
let match;
|
|
302
|
+
while ((match = pathPattern.exec(content)) !== null) {
|
|
303
|
+
routes.push({
|
|
304
|
+
method: 'ALL',
|
|
305
|
+
path: '/' + match[1],
|
|
306
|
+
handler: match[2],
|
|
307
|
+
file: relative(dir, filePath),
|
|
308
|
+
source: 'django',
|
|
309
|
+
auth: false,
|
|
310
|
+
description: '',
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return routes;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── FastAPI / Flask ─────────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
function scanFastAPIRoutes(dir) {
|
|
321
|
+
const routes = [];
|
|
322
|
+
const pattern = /@(?:app|router)\s*\.\s*(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
323
|
+
|
|
324
|
+
const pyFiles = findFiles(dir, /\.py$/);
|
|
325
|
+
for (const filePath of pyFiles) {
|
|
326
|
+
const content = readFileSafe(filePath);
|
|
327
|
+
if (!content) continue;
|
|
328
|
+
|
|
329
|
+
let match;
|
|
330
|
+
const regex = new RegExp(pattern.source, 'gi');
|
|
331
|
+
while ((match = regex.exec(content)) !== null) {
|
|
332
|
+
routes.push({
|
|
333
|
+
method: match[1].toUpperCase(),
|
|
334
|
+
path: match[2],
|
|
335
|
+
handler: extractPythonFunctionName(content, match.index),
|
|
336
|
+
file: relative(dir, filePath),
|
|
337
|
+
source: 'fastapi',
|
|
338
|
+
auth: content.includes('Depends(') && content.includes('auth'),
|
|
339
|
+
description: extractPythonDocstring(content, match.index),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return routes;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
function readFileSafe(path) {
|
|
350
|
+
try { return readFileSync(path, 'utf-8'); } catch { return null; }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isJSFile(path) {
|
|
354
|
+
return /\.(js|mjs|cjs|ts|tsx|jsx)$/.test(path);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function walkRouteDirs(dir, callback) {
|
|
358
|
+
if (!existsSync(dir)) return;
|
|
359
|
+
try {
|
|
360
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
361
|
+
for (const entry of entries) {
|
|
362
|
+
if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
363
|
+
const fullPath = join(dir, entry.name);
|
|
364
|
+
if (entry.isDirectory()) {
|
|
365
|
+
walkRouteDirs(fullPath, callback);
|
|
366
|
+
} else if (entry.isFile()) {
|
|
367
|
+
callback(fullPath);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} catch { /* skip */ }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function findFiles(dir, pattern, maxDepth = 5) {
|
|
374
|
+
const results = [];
|
|
375
|
+
function walk(d, depth) {
|
|
376
|
+
if (depth > maxDepth || !existsSync(d)) return;
|
|
377
|
+
try {
|
|
378
|
+
const entries = readdirSync(d, { withFileTypes: true });
|
|
379
|
+
for (const entry of entries) {
|
|
380
|
+
if (IGNORE_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
|
|
381
|
+
const fullPath = join(d, entry.name);
|
|
382
|
+
if (entry.isDirectory()) walk(fullPath, depth + 1);
|
|
383
|
+
else if (pattern.test(entry.name)) results.push(fullPath);
|
|
384
|
+
}
|
|
385
|
+
} catch { /* skip */ }
|
|
386
|
+
}
|
|
387
|
+
walk(dir, 0);
|
|
388
|
+
return results;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function hasAuthCheck(content) {
|
|
392
|
+
const authPatterns = [
|
|
393
|
+
/getServerSession/, /getSession/, /getToken/,
|
|
394
|
+
/auth\(\)/, /authenticate/, /isAuthenticated/,
|
|
395
|
+
/requireAuth/, /withAuth/, /protect/,
|
|
396
|
+
/Authorization/, /Bearer/, /jwt\.verify/,
|
|
397
|
+
/req\.user/, /req\.auth/,
|
|
398
|
+
];
|
|
399
|
+
return authPatterns.some(p => p.test(content));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function hasAuthMiddleware(content, routePath) {
|
|
403
|
+
// Check if route has auth middleware before handler
|
|
404
|
+
const pattern = new RegExp(
|
|
405
|
+
`['"\`]${routePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"\`]\\s*,\\s*(auth|protect|requireAuth|isAuthenticated|authenticate)`,
|
|
406
|
+
'i'
|
|
407
|
+
);
|
|
408
|
+
return pattern.test(content) || hasAuthCheck(content);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function detectMethodsFromHandler(content) {
|
|
412
|
+
const methods = new Set();
|
|
413
|
+
if (/req\.method\s*===?\s*['"]GET['"]/i.test(content) || /case\s+['"]GET['"]/i.test(content)) methods.add('GET');
|
|
414
|
+
if (/req\.method\s*===?\s*['"]POST['"]/i.test(content) || /case\s+['"]POST['"]/i.test(content)) methods.add('POST');
|
|
415
|
+
if (/req\.method\s*===?\s*['"]PUT['"]/i.test(content) || /case\s+['"]PUT['"]/i.test(content)) methods.add('PUT');
|
|
416
|
+
if (/req\.method\s*===?\s*['"]DELETE['"]/i.test(content) || /case\s+['"]DELETE['"]/i.test(content)) methods.add('DELETE');
|
|
417
|
+
if (/req\.method\s*===?\s*['"]PATCH['"]/i.test(content) || /case\s+['"]PATCH['"]/i.test(content)) methods.add('PATCH');
|
|
418
|
+
if (methods.size === 0) methods.add('ALL'); // Default handler
|
|
419
|
+
return [...methods];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function extractHandlerName(content, matchIndex) {
|
|
423
|
+
// Look for function name after the route path
|
|
424
|
+
const after = content.substring(matchIndex, matchIndex + 200);
|
|
425
|
+
const fnMatch = after.match(/,\s*(?:async\s+)?(\w+)/);
|
|
426
|
+
return fnMatch ? fnMatch[1] : '';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function extractJSDocDescription(content, methodName) {
|
|
430
|
+
// Look for JSDoc comment before the method export
|
|
431
|
+
const pattern = new RegExp(`/\\*\\*\\s*\\n([^*]*(?:\\*[^/][^*]*)*?)\\*/\\s*\\n\\s*export\\s+(?:async\\s+)?function\\s+${methodName || ''}`, 'i');
|
|
432
|
+
const match = pattern.exec(content);
|
|
433
|
+
if (match) {
|
|
434
|
+
return match[1].replace(/\s*\*\s*/g, ' ').trim().split('.')[0];
|
|
435
|
+
}
|
|
436
|
+
return '';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function extractNearbyComment(content, index) {
|
|
440
|
+
// Look for comment on the line before the match
|
|
441
|
+
const before = content.substring(Math.max(0, index - 200), index);
|
|
442
|
+
const lines = before.split('\n');
|
|
443
|
+
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 3); i--) {
|
|
444
|
+
const line = lines[i].trim();
|
|
445
|
+
if (line.startsWith('//')) return line.replace(/^\/\/\s*/, '');
|
|
446
|
+
if (line.startsWith('*') && !line.startsWith('*/')) return line.replace(/^\*\s*/, '');
|
|
447
|
+
}
|
|
448
|
+
return '';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function extractPythonFunctionName(content, index) {
|
|
452
|
+
const after = content.substring(index, index + 300);
|
|
453
|
+
const match = after.match(/def\s+(\w+)/);
|
|
454
|
+
return match ? match[1] : '';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function extractPythonDocstring(content, index) {
|
|
458
|
+
const after = content.substring(index, index + 500);
|
|
459
|
+
const match = after.match(/"""([^"]+)"""/);
|
|
460
|
+
return match ? match[1].trim().split('\n')[0] : '';
|
|
461
|
+
}
|