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.
- package/dist/ast/extract-go.d.ts +23 -0
- package/dist/ast/extract-go.js +331 -0
- package/dist/ast/extract-python.d.ts +25 -0
- package/dist/ast/extract-python.js +388 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +96 -0
- package/dist/detectors/routes.js +51 -55
- package/dist/detectors/schema.js +29 -3
- package/dist/eval.d.ts +5 -0
- package/dist/eval.js +184 -0
- package/dist/index.js +99 -15
- package/dist/scanner.js +3 -3
- package/dist/telemetry.d.ts +38 -0
- package/dist/telemetry.js +257 -0
- package/dist/types.d.ts +35 -0
- package/eval/README.md +36 -0
- package/eval/fixtures/express-prisma/ground-truth.json +31 -0
- package/eval/fixtures/express-prisma/repo.json +19 -0
- package/eval/fixtures/fastapi-sqlalchemy/ground-truth.json +29 -0
- package/eval/fixtures/fastapi-sqlalchemy/repo.json +16 -0
- package/eval/fixtures/gin-gorm/ground-truth.json +25 -0
- package/eval/fixtures/gin-gorm/repo.json +16 -0
- package/eval/fixtures/hono-monorepo/ground-truth.json +33 -0
- package/eval/fixtures/hono-monorepo/repo.json +20 -0
- package/eval/fixtures/nextjs-drizzle/ground-truth.json +37 -0
- package/eval/fixtures/nextjs-drizzle/repo.json +21 -0
- package/package.json +3 -2
|
@@ -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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/detectors/routes.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
683
|
-
|
|
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
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
}
|