codesight 1.3.2 → 1.5.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.
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Python AST extraction via subprocess.
3
+ * Spawns python3 with an inline script using stdlib `ast` module.
4
+ * Zero dependencies — if the project uses Python, the interpreter is there.
5
+ *
6
+ * Extracts:
7
+ * - FastAPI/Flask route decorators with precise path + method
8
+ * - Django urlpatterns with path() calls
9
+ * - SQLAlchemy model classes with Column types, flags, and relationships
10
+ */
11
+ import { execFile } from "node:child_process";
12
+ import { promisify } from "node:util";
13
+ const execFileP = promisify(execFile);
14
+ // The Python script that does AST parsing via stdlib
15
+ // Outputs JSON to stdout
16
+ const PYTHON_ROUTE_SCRIPT = `
17
+ import ast, json, sys
18
+
19
+ def extract_routes(source, filename):
20
+ try:
21
+ tree = ast.parse(source, filename)
22
+ except SyntaxError:
23
+ return []
24
+
25
+ routes = []
26
+
27
+ for node in ast.walk(tree):
28
+ # FastAPI/Flask style: @router.get("/path") or @app.route("/path", methods=["GET","POST"])
29
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
30
+ for dec in node.decorator_list:
31
+ route = parse_route_decorator(dec)
32
+ if route:
33
+ for r in route:
34
+ routes.append(r)
35
+
36
+ # Django: path("url", view) in urlpatterns list
37
+ if isinstance(node, ast.Assign):
38
+ for target in node.targets:
39
+ if isinstance(target, ast.Name) and target.id == 'urlpatterns':
40
+ if isinstance(node.value, (ast.List, ast.BinOp)):
41
+ routes.extend(parse_urlpatterns(node.value))
42
+
43
+ return routes
44
+
45
+ def parse_route_decorator(dec):
46
+ results = []
47
+
48
+ # @router.get("/path") / @app.post("/path") — Attribute call
49
+ if isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute):
50
+ method_name = dec.func.attr.upper()
51
+ methods_map = {'GET':'GET','POST':'POST','PUT':'PUT','PATCH':'PATCH',
52
+ 'DELETE':'DELETE','OPTIONS':'OPTIONS','HEAD':'HEAD'}
53
+
54
+ if method_name in methods_map and dec.args:
55
+ path = get_str(dec.args[0])
56
+ if path is not None:
57
+ results.append({'method': methods_map[method_name], 'path': path})
58
+
59
+ # @app.route("/path", methods=["GET","POST"])
60
+ if dec.func.attr == 'route' and dec.args:
61
+ path = get_str(dec.args[0])
62
+ if path is not None:
63
+ methods = ['GET']
64
+ for kw in dec.keywords:
65
+ if kw.arg == 'methods' and isinstance(kw.value, ast.List):
66
+ methods = [get_str(e) for e in kw.value.elts if get_str(e)]
67
+ for m in methods:
68
+ results.append({'method': m.upper(), 'path': path})
69
+
70
+ # @app.api_route("/path", methods=["GET","POST"]) — FastAPI api_route
71
+ if isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute):
72
+ if dec.func.attr == 'api_route' and dec.args:
73
+ path = get_str(dec.args[0])
74
+ if path is not None:
75
+ methods = ['GET']
76
+ for kw in dec.keywords:
77
+ if kw.arg == 'methods' and isinstance(kw.value, ast.List):
78
+ methods = [get_str(e) for e in kw.value.elts if get_str(e)]
79
+ for m in methods:
80
+ results.append({'method': m.upper(), 'path': path})
81
+
82
+ return results if results else None
83
+
84
+ def parse_urlpatterns(node):
85
+ routes = []
86
+ elements = []
87
+ if isinstance(node, ast.List):
88
+ elements = node.elts
89
+ elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
90
+ # urlpatterns = [...] + [...]
91
+ if isinstance(node.left, ast.List):
92
+ elements.extend(node.left.elts)
93
+ if isinstance(node.right, ast.List):
94
+ elements.extend(node.right.elts)
95
+
96
+ for elt in elements:
97
+ if isinstance(elt, ast.Call):
98
+ func_name = ''
99
+ if isinstance(elt.func, ast.Name):
100
+ func_name = elt.func.id
101
+ elif isinstance(elt.func, ast.Attribute):
102
+ func_name = elt.func.attr
103
+
104
+ if func_name in ('path', 're_path', 'url') and elt.args:
105
+ path_str = get_str(elt.args[0])
106
+ if path_str is not None:
107
+ routes.append({'method': 'ALL', 'path': '/' + path_str.lstrip('/')})
108
+
109
+ return routes
110
+
111
+ def get_str(node):
112
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
113
+ return node.value
114
+ if isinstance(node, ast.Str): # Python 3.7
115
+ return node.s
116
+ return None
117
+
118
+ source = sys.stdin.read()
119
+ filename = sys.argv[1] if len(sys.argv) > 1 else '<stdin>'
120
+ result = extract_routes(source, filename)
121
+ print(json.dumps(result))
122
+ `;
123
+ const PYTHON_SCHEMA_SCRIPT = `
124
+ import ast, json, sys
125
+
126
+ def extract_models(source, filename):
127
+ try:
128
+ tree = ast.parse(source, filename)
129
+ except SyntaxError:
130
+ return []
131
+
132
+ models = []
133
+
134
+ for node in ast.walk(tree):
135
+ if not isinstance(node, ast.ClassDef):
136
+ continue
137
+
138
+ # Check if inherits from Base, Model, DeclarativeBase, db.Model
139
+ is_model = False
140
+ for base in node.bases:
141
+ name = ''
142
+ if isinstance(base, ast.Name):
143
+ name = base.id
144
+ elif isinstance(base, ast.Attribute):
145
+ name = base.attr
146
+ if name in ('Base', 'Model', 'DeclarativeBase', 'AbstractBase'):
147
+ is_model = True
148
+ break
149
+ if not is_model:
150
+ continue
151
+
152
+ fields = []
153
+ relations = []
154
+
155
+ for item in node.body:
156
+ # Column assignments: field = Column(Type, ...) or field: Mapped[Type] = mapped_column(...)
157
+ if isinstance(item, ast.Assign):
158
+ for target in item.targets:
159
+ if isinstance(target, ast.Name):
160
+ field_info = parse_column_or_rel(target.id, item.value)
161
+ if field_info:
162
+ if field_info['kind'] == 'field':
163
+ fields.append(field_info)
164
+ else:
165
+ relations.append(field_info)
166
+
167
+ if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
168
+ field_name = item.target.id
169
+ if field_name.startswith('__'):
170
+ continue
171
+ if item.value:
172
+ field_info = parse_column_or_rel(field_name, item.value)
173
+ if field_info:
174
+ if field_info['kind'] == 'field':
175
+ # Try to get type from annotation
176
+ ann_type = extract_mapped_type(item.annotation)
177
+ if ann_type and field_info['type'] == 'unknown':
178
+ field_info['type'] = ann_type
179
+ fields.append(field_info)
180
+ else:
181
+ relations.append(field_info)
182
+
183
+ if fields:
184
+ models.append({
185
+ 'name': node.name,
186
+ 'fields': fields,
187
+ 'relations': relations,
188
+ })
189
+
190
+ return models
191
+
192
+ def parse_column_or_rel(name, value):
193
+ if name.startswith('__'):
194
+ return None
195
+
196
+ # relationship("Model")
197
+ if isinstance(value, ast.Call):
198
+ func_name = get_func_name(value)
199
+
200
+ if func_name in ('relationship', 'db.relationship'):
201
+ target = ''
202
+ if value.args:
203
+ target = get_str(value.args[0]) or ''
204
+ return {'kind': 'relation', 'name': name, 'target': target}
205
+
206
+ if func_name in ('Column', 'db.Column', 'mapped_column'):
207
+ col_type = 'unknown'
208
+ flags = []
209
+
210
+ for arg in value.args:
211
+ t = get_type_name(arg)
212
+ if t:
213
+ col_type = t
214
+ if isinstance(arg, ast.Call) and get_func_name(arg) == 'ForeignKey':
215
+ flags.append('fk')
216
+
217
+ for kw in value.keywords:
218
+ if kw.arg == 'primary_key' and is_true(kw.value):
219
+ flags.append('pk')
220
+ elif kw.arg == 'unique' and is_true(kw.value):
221
+ flags.append('unique')
222
+ elif kw.arg == 'nullable' and is_true(kw.value):
223
+ flags.append('nullable')
224
+ elif kw.arg == 'default':
225
+ flags.append('default')
226
+ elif kw.arg == 'index' and is_true(kw.value):
227
+ flags.append('index')
228
+
229
+ return {'kind': 'field', 'name': name, 'type': col_type, 'flags': flags}
230
+
231
+ return None
232
+
233
+ def extract_mapped_type(annotation):
234
+ # Mapped[int] or Mapped[Optional[str]]
235
+ if isinstance(annotation, ast.Subscript):
236
+ if isinstance(annotation.value, ast.Name) and annotation.value.id == 'Mapped':
237
+ slice_node = annotation.slice
238
+ if isinstance(slice_node, ast.Name):
239
+ return slice_node.id
240
+ if isinstance(slice_node, ast.Subscript) and isinstance(slice_node.value, ast.Name):
241
+ if slice_node.value.id == 'Optional':
242
+ inner = slice_node.slice
243
+ if isinstance(inner, ast.Name):
244
+ return inner.id
245
+ return None
246
+
247
+ def get_func_name(node):
248
+ if isinstance(node.func, ast.Name):
249
+ return node.func.id
250
+ if isinstance(node.func, ast.Attribute):
251
+ if isinstance(node.func.value, ast.Name):
252
+ return node.func.value.id + '.' + node.func.attr
253
+ return node.func.attr
254
+ return ''
255
+
256
+ def get_type_name(node):
257
+ if isinstance(node, ast.Name):
258
+ known = {'String','Integer','Boolean','Float','Text','DateTime','JSON',
259
+ 'UUID','BigInteger','SmallInteger','Numeric','Date','Time',
260
+ 'LargeBinary','Enum','ARRAY','JSONB'}
261
+ if node.id in known:
262
+ return node.id
263
+ if isinstance(node, ast.Call):
264
+ return get_type_name(node.func)
265
+ if isinstance(node, ast.Attribute):
266
+ return node.attr
267
+ return None
268
+
269
+ def get_str(node):
270
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
271
+ return node.value
272
+ return None
273
+
274
+ def is_true(node):
275
+ if isinstance(node, ast.Constant):
276
+ return node.value is True
277
+ if isinstance(node, ast.NameConstant): # Python 3.7
278
+ return node.value is True
279
+ return False
280
+
281
+ source = sys.stdin.read()
282
+ filename = sys.argv[1] if len(sys.argv) > 1 else '<stdin>'
283
+ result = extract_models(source, filename)
284
+ print(json.dumps(result))
285
+ `;
286
+ let pythonAvailable = null;
287
+ let pythonCmd = "python3";
288
+ async function findPython() {
289
+ if (pythonAvailable !== null)
290
+ return pythonAvailable;
291
+ for (const cmd of ["python3", "python"]) {
292
+ try {
293
+ const { stdout } = await execFileP(cmd, ["--version"], { timeout: 3000 });
294
+ if (stdout.includes("Python 3")) {
295
+ pythonCmd = cmd;
296
+ pythonAvailable = true;
297
+ return true;
298
+ }
299
+ }
300
+ catch { }
301
+ }
302
+ pythonAvailable = false;
303
+ return false;
304
+ }
305
+ import { spawn } from "node:child_process";
306
+ async function runPythonWithStdin(script, source, filename) {
307
+ if (!(await findPython()))
308
+ return null;
309
+ return new Promise((resolve) => {
310
+ const proc = spawn(pythonCmd, ["-c", script, filename], {
311
+ timeout: 10000,
312
+ env: { ...process.env, PYTHONDONTWRITEBYTECODE: "1" },
313
+ stdio: ["pipe", "pipe", "pipe"],
314
+ });
315
+ let stdout = "";
316
+ let stderr = "";
317
+ proc.stdout.on("data", (data) => { stdout += data.toString(); });
318
+ proc.stderr.on("data", (data) => { stderr += data.toString(); });
319
+ proc.on("close", (code) => {
320
+ if (code !== 0 || !stdout.trim()) {
321
+ resolve(null);
322
+ return;
323
+ }
324
+ try {
325
+ resolve(JSON.parse(stdout.trim()));
326
+ }
327
+ catch {
328
+ resolve(null);
329
+ }
330
+ });
331
+ proc.on("error", () => resolve(null));
332
+ proc.stdin.write(source);
333
+ proc.stdin.end();
334
+ });
335
+ }
336
+ /**
337
+ * Extract routes from a Python file using AST.
338
+ * Returns routes with confidence: "ast", or null if Python is unavailable.
339
+ */
340
+ export async function extractPythonRoutesAST(filePath, content, framework, tags) {
341
+ const result = await runPythonWithStdin(PYTHON_ROUTE_SCRIPT, content, filePath);
342
+ if (!result || !Array.isArray(result) || result.length === 0)
343
+ return null;
344
+ return result.map((r) => ({
345
+ method: r.method,
346
+ path: r.path,
347
+ file: filePath,
348
+ tags,
349
+ framework,
350
+ params: extractPathParams(r.path),
351
+ confidence: "ast",
352
+ }));
353
+ }
354
+ /**
355
+ * Extract SQLAlchemy models from a Python file using AST.
356
+ * Returns models with confidence: "ast", or null if Python is unavailable.
357
+ */
358
+ export async function extractSQLAlchemyAST(filePath, content) {
359
+ const result = await runPythonWithStdin(PYTHON_SCHEMA_SCRIPT, content, filePath);
360
+ if (!result || !Array.isArray(result) || result.length === 0)
361
+ return null;
362
+ return result.map((m) => ({
363
+ name: m.name,
364
+ fields: (m.fields || []).map((f) => ({
365
+ name: f.name,
366
+ type: f.type || "unknown",
367
+ flags: f.flags || [],
368
+ })),
369
+ relations: (m.relations || []).map((r) => `${r.name}: ${r.target}`),
370
+ orm: "sqlalchemy",
371
+ confidence: "ast",
372
+ }));
373
+ }
374
+ /**
375
+ * Check if Python 3 is available on this system.
376
+ */
377
+ export async function isPythonAvailable() {
378
+ return findPython();
379
+ }
380
+ function extractPathParams(path) {
381
+ const params = [];
382
+ // FastAPI: {param} / Flask: <param> / Django: <type:param>
383
+ const regex = /[{<](?:\w+:)?(\w+)[}>]/g;
384
+ let m;
385
+ while ((m = regex.exec(path)) !== null)
386
+ params.push(m[1]);
387
+ return params;
388
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Configuration loader: reads codesight.config.(ts|js|json) from project root.
3
+ */
4
+ import type { CodesightConfig } from "./types.js";
5
+ /**
6
+ * Load config from project root. Returns empty config if no config file found.
7
+ */
8
+ export declare function loadConfig(root: string): Promise<CodesightConfig>;
9
+ /**
10
+ * Merges CLI args with config file values (CLI takes precedence).
11
+ */
12
+ export declare function mergeCliConfig(config: CodesightConfig, cli: {
13
+ maxDepth?: number;
14
+ outputDir?: string;
15
+ profile?: string;
16
+ }): CodesightConfig;
package/dist/config.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Configuration loader: reads codesight.config.(ts|js|json) from project root.
3
+ */
4
+ import { readFile, stat } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ const CONFIG_FILES = [
8
+ "codesight.config.ts",
9
+ "codesight.config.js",
10
+ "codesight.config.mjs",
11
+ "codesight.config.json",
12
+ ];
13
+ async function fileExists(path) {
14
+ try {
15
+ await stat(path);
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
22
+ /**
23
+ * Load config from project root. Returns empty config if no config file found.
24
+ */
25
+ export async function loadConfig(root) {
26
+ for (const filename of CONFIG_FILES) {
27
+ const configPath = join(root, filename);
28
+ if (!(await fileExists(configPath)))
29
+ continue;
30
+ try {
31
+ if (filename.endsWith(".json")) {
32
+ const content = await readFile(configPath, "utf-8");
33
+ return JSON.parse(content);
34
+ }
35
+ if (filename.endsWith(".ts")) {
36
+ // Try loading with tsx or ts-node if available
37
+ return await loadTsConfig(configPath, root);
38
+ }
39
+ // JS/MJS — dynamic import
40
+ const module = await import(pathToFileURL(configPath).href);
41
+ return (module.default || module);
42
+ }
43
+ catch (err) {
44
+ console.warn(` Warning: failed to load ${filename}: ${err.message}`);
45
+ return {};
46
+ }
47
+ }
48
+ // Also check package.json "codesight" field
49
+ try {
50
+ const pkgPath = join(root, "package.json");
51
+ if (await fileExists(pkgPath)) {
52
+ const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
53
+ if (pkg.codesight && typeof pkg.codesight === "object") {
54
+ return pkg.codesight;
55
+ }
56
+ }
57
+ }
58
+ catch { }
59
+ return {};
60
+ }
61
+ async function loadTsConfig(configPath, _root) {
62
+ // Strategy 1: try tsx via dynamic import of the .ts file directly
63
+ // (works if tsx or ts-node is installed)
64
+ try {
65
+ const module = await import(pathToFileURL(configPath).href);
66
+ return (module.default || module);
67
+ }
68
+ catch { }
69
+ // Strategy 2: read as text and extract JSON-like config
70
+ // (fallback for when no TS loader is available)
71
+ const content = await readFile(configPath, "utf-8");
72
+ // Try to extract the config object from simple export default { ... }
73
+ const match = content.match(/export\s+default\s+({[\s\S]*})\s*;?\s*$/m);
74
+ if (match) {
75
+ try {
76
+ // Use Function constructor to evaluate the object literal
77
+ // Safe here since this is user's own config file in their project
78
+ const fn = new Function(`return (${match[1]})`);
79
+ return fn();
80
+ }
81
+ catch { }
82
+ }
83
+ console.warn(` Warning: cannot load codesight.config.ts (install tsx for TS config support)`);
84
+ return {};
85
+ }
86
+ /**
87
+ * Merges CLI args with config file values (CLI takes precedence).
88
+ */
89
+ export function mergeCliConfig(config, cli) {
90
+ return {
91
+ ...config,
92
+ maxDepth: cli.maxDepth ?? config.maxDepth,
93
+ outputDir: cli.outputDir ?? config.outputDir,
94
+ profile: cli.profile ?? config.profile,
95
+ };
96
+ }
@@ -2,6 +2,8 @@ import { relative, basename } from "node:path";
2
2
  import { readFileSafe } from "../scanner.js";
3
3
  import { loadTypeScript } from "../ast/loader.js";
4
4
  import { extractRoutesAST } from "../ast/extract-routes.js";
5
+ import { extractPythonRoutesAST } from "../ast/extract-python.js";
6
+ import { extractGoRoutesStructured } from "../ast/extract-go.js";
5
7
  const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
6
8
  const TAG_PATTERNS = [
7
9
  ["auth", [/auth/i, /jwt/i, /token/i, /session/i, /bearer/i, /passport/i, /clerk/i, /betterAuth/i, /better-auth/i]],
@@ -581,6 +583,14 @@ async function detectFastAPIRoutes(files, project) {
581
583
  if (!content.includes("fastapi") && !content.includes("FastAPI") && !content.includes("APIRouter"))
582
584
  continue;
583
585
  const rel = relative(project.root, file);
586
+ const tags = detectTags(content);
587
+ // Try Python AST first
588
+ const astRoutes = await extractPythonRoutesAST(rel, content, "fastapi", tags);
589
+ if (astRoutes && astRoutes.length > 0) {
590
+ routes.push(...astRoutes);
591
+ continue;
592
+ }
593
+ // Fallback to regex
584
594
  const routePattern = /@\w+\s*\.\s*(get|post|put|patch|delete|options)\s*\(\s*['"]([^'"]+)['"]/gi;
585
595
  let match;
586
596
  while ((match = routePattern.exec(content)) !== null) {
@@ -588,7 +598,7 @@ async function detectFastAPIRoutes(files, project) {
588
598
  method: match[1].toUpperCase(),
589
599
  path: match[2],
590
600
  file: rel,
591
- tags: detectTags(content),
601
+ tags,
592
602
  framework: "fastapi",
593
603
  });
594
604
  }
@@ -604,6 +614,14 @@ async function detectFlaskRoutes(files, project) {
604
614
  if (!content.includes("flask") && !content.includes("Flask") && !content.includes("Blueprint"))
605
615
  continue;
606
616
  const rel = relative(project.root, file);
617
+ const tags = detectTags(content);
618
+ // Try Python AST first
619
+ const astRoutes = await extractPythonRoutesAST(rel, content, "flask", tags);
620
+ if (astRoutes && astRoutes.length > 0) {
621
+ routes.push(...astRoutes);
622
+ continue;
623
+ }
624
+ // Fallback to regex
607
625
  const routePattern = /@(?:app|bp|blueprint|\w+)\s*\.\s*route\s*\(\s*['"]([^'"]+)['"](?:\s*,\s*methods\s*=\s*\[([^\]]+)\])?\s*\)/gi;
608
626
  let match;
609
627
  while ((match = routePattern.exec(content)) !== null) {
@@ -616,7 +634,7 @@ async function detectFlaskRoutes(files, project) {
616
634
  method,
617
635
  path,
618
636
  file: rel,
619
- tags: detectTags(content),
637
+ tags,
620
638
  framework: "flask",
621
639
  });
622
640
  }
@@ -631,7 +649,14 @@ async function detectDjangoRoutes(files, project) {
631
649
  for (const file of pyFiles) {
632
650
  const content = await readFileSafe(file);
633
651
  const rel = relative(project.root, file);
634
- // path("api/v1/users/", views.UserView.as_view())
652
+ const tags = detectTags(content);
653
+ // Try Python AST first
654
+ const astRoutes = await extractPythonRoutesAST(rel, content, "django", tags);
655
+ if (astRoutes && astRoutes.length > 0) {
656
+ routes.push(...astRoutes);
657
+ continue;
658
+ }
659
+ // Fallback to regex
635
660
  const pathPattern = /path\s*\(\s*['"]([^'"]*)['"]\s*,/g;
636
661
  let match;
637
662
  while ((match = pathPattern.exec(content)) !== null) {
@@ -639,7 +664,7 @@ async function detectDjangoRoutes(files, project) {
639
664
  method: "ALL",
640
665
  path: "/" + match[1],
641
666
  file: rel,
642
- tags: detectTags(content),
667
+ tags,
643
668
  framework: "django",
644
669
  });
645
670
  }
@@ -653,72 +678,43 @@ async function detectGoRoutes(files, project, fw) {
653
678
  for (const file of goFiles) {
654
679
  const content = await readFileSafe(file);
655
680
  const rel = relative(project.root, file);
656
- if (fw === "gin") {
657
- const pattern = /\.\s*(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*["']([^"']+)["']/g;
658
- let match;
659
- while ((match = pattern.exec(content)) !== null) {
660
- routes.push({
661
- method: match[1],
662
- path: match[2],
663
- file: rel,
664
- tags: detectTags(content),
665
- framework: fw,
666
- });
667
- }
668
- }
669
- else if (fw === "fiber") {
670
- const pattern = /\.\s*(Get|Post|Put|Patch|Delete|Options|Head)\s*\(\s*["']([^"']+)["']/g;
671
- let match;
672
- while ((match = pattern.exec(content)) !== null) {
673
- routes.push({
674
- method: match[1].toUpperCase(),
675
- path: match[2],
676
- file: rel,
677
- tags: detectTags(content),
678
- framework: fw,
679
- });
680
- }
681
+ const tags = detectTags(content);
682
+ // Use structured parser (brace-tracking + group prefix resolution)
683
+ const structuredRoutes = extractGoRoutesStructured(rel, content, fw, tags);
684
+ if (structuredRoutes.length > 0) {
685
+ routes.push(...structuredRoutes);
686
+ continue;
681
687
  }
682
- else if (fw === "echo") {
683
- // e.GET("/path", handler) or g.POST("/path", handler)
688
+ // Fallback to simple regex for files where structured parser found nothing
689
+ if (fw === "gin" || fw === "echo") {
684
690
  const pattern = /\.\s*(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*["']([^"']+)["']/g;
685
691
  let match;
686
692
  while ((match = pattern.exec(content)) !== null) {
687
- routes.push({
688
- method: match[1],
689
- path: match[2],
690
- file: rel,
691
- tags: detectTags(content),
692
- framework: fw,
693
- });
693
+ routes.push({ method: match[1], path: match[2], file: rel, tags, framework: fw });
694
694
  }
695
695
  }
696
- else if (fw === "chi") {
697
- // r.Get("/path", handler), r.Post("/path", handler)
696
+ else if (fw === "fiber" || fw === "chi") {
698
697
  const pattern = /\.\s*(Get|Post|Put|Patch|Delete|Options|Head)\s*\(\s*["']([^"']+)["']/g;
699
698
  let match;
700
699
  while ((match = pattern.exec(content)) !== null) {
701
- routes.push({
702
- method: match[1].toUpperCase(),
703
- path: match[2],
704
- file: rel,
705
- tags: detectTags(content),
706
- framework: fw,
707
- });
700
+ routes.push({ method: match[1].toUpperCase(), path: match[2], file: rel, tags, framework: fw });
708
701
  }
709
702
  }
710
703
  else {
711
- // net/http: http.HandleFunc("/path", handler) or mux.HandleFunc("/path", handler)
704
+ // net/http
712
705
  const pattern = /(?:HandleFunc|Handle)\s*\(\s*["']([^"']+)["']/g;
713
706
  let match;
714
707
  while ((match = pattern.exec(content)) !== null) {
715
- routes.push({
716
- method: "ALL",
717
- path: match[1],
718
- file: rel,
719
- tags: detectTags(content),
720
- framework: fw,
721
- });
708
+ // Go 1.22+: "GET /path" patterns
709
+ const pathStr = match[1];
710
+ let method = "ALL";
711
+ let path = pathStr;
712
+ const methodMatch = pathStr.match(/^(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+(\/.*)/);
713
+ if (methodMatch) {
714
+ method = methodMatch[1];
715
+ path = methodMatch[2];
716
+ }
717
+ routes.push({ method, path, file: rel, tags, framework: fw });
722
718
  }
723
719
  }
724
720
  }