@toolbaux/guardian 0.1.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/LICENSE +21 -0
- package/README.md +366 -0
- package/dist/adapters/csharp-adapter.js +149 -0
- package/dist/adapters/go-adapter.js +96 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/java-adapter.js +122 -0
- package/dist/adapters/python-adapter.js +183 -0
- package/dist/adapters/runner.js +69 -0
- package/dist/adapters/types.js +1 -0
- package/dist/adapters/typescript-adapter.js +179 -0
- package/dist/benchmarking/framework.js +91 -0
- package/dist/cli.js +343 -0
- package/dist/commands/analyze-depth.js +43 -0
- package/dist/commands/api-spec-extractor.js +52 -0
- package/dist/commands/breaking-change-analyzer.js +334 -0
- package/dist/commands/config-compliance.js +219 -0
- package/dist/commands/constraints.js +221 -0
- package/dist/commands/context.js +101 -0
- package/dist/commands/data-flow-tracer.js +291 -0
- package/dist/commands/dependency-impact-analyzer.js +27 -0
- package/dist/commands/diff.js +146 -0
- package/dist/commands/discrepancy.js +71 -0
- package/dist/commands/doc-generate.js +163 -0
- package/dist/commands/doc-html.js +120 -0
- package/dist/commands/drift.js +88 -0
- package/dist/commands/extract.js +16 -0
- package/dist/commands/feature-context.js +116 -0
- package/dist/commands/generate.js +339 -0
- package/dist/commands/guard.js +182 -0
- package/dist/commands/init.js +209 -0
- package/dist/commands/intel.js +20 -0
- package/dist/commands/license-dependency-auditor.js +33 -0
- package/dist/commands/performance-hotspot-profiler.js +42 -0
- package/dist/commands/search.js +314 -0
- package/dist/commands/security-boundary-auditor.js +359 -0
- package/dist/commands/simulate.js +294 -0
- package/dist/commands/summary.js +27 -0
- package/dist/commands/test-coverage-mapper.js +264 -0
- package/dist/commands/verify-drift.js +62 -0
- package/dist/config.js +441 -0
- package/dist/extract/ai-context-hints.js +107 -0
- package/dist/extract/analyzers/backend.js +1704 -0
- package/dist/extract/analyzers/depth.js +264 -0
- package/dist/extract/analyzers/frontend.js +2221 -0
- package/dist/extract/api-usage-tracker.js +19 -0
- package/dist/extract/cache.js +53 -0
- package/dist/extract/codebase-intel.js +190 -0
- package/dist/extract/compress.js +452 -0
- package/dist/extract/context-block.js +356 -0
- package/dist/extract/contracts.js +183 -0
- package/dist/extract/discrepancies.js +233 -0
- package/dist/extract/docs-loader.js +110 -0
- package/dist/extract/docs.js +2379 -0
- package/dist/extract/drift.js +1578 -0
- package/dist/extract/duplicates.js +435 -0
- package/dist/extract/feature-arcs.js +138 -0
- package/dist/extract/graph.js +76 -0
- package/dist/extract/html-doc.js +1409 -0
- package/dist/extract/ignore.js +45 -0
- package/dist/extract/index.js +455 -0
- package/dist/extract/llm-client.js +159 -0
- package/dist/extract/pattern-registry.js +141 -0
- package/dist/extract/product-doc.js +497 -0
- package/dist/extract/python.js +1202 -0
- package/dist/extract/runtime.js +193 -0
- package/dist/extract/schema-evolution-validator.js +35 -0
- package/dist/extract/test-gap-analyzer.js +20 -0
- package/dist/extract/tests.js +74 -0
- package/dist/extract/types.js +1 -0
- package/dist/extract/validate-backend.js +30 -0
- package/dist/extract/writer.js +11 -0
- package/dist/output-layout.js +37 -0
- package/dist/project-discovery.js +309 -0
- package/dist/schema/architecture.js +350 -0
- package/dist/schema/feature-spec.js +89 -0
- package/dist/schema/index.js +8 -0
- package/dist/schema/ux.js +46 -0
- package/package.json +75 -0
|
@@ -0,0 +1,1202 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import Parser from "tree-sitter";
|
|
4
|
+
import Python from "tree-sitter-python";
|
|
5
|
+
const HTTP_METHODS = new Set([
|
|
6
|
+
"get",
|
|
7
|
+
"post",
|
|
8
|
+
"put",
|
|
9
|
+
"patch",
|
|
10
|
+
"delete",
|
|
11
|
+
"options",
|
|
12
|
+
"head"
|
|
13
|
+
]);
|
|
14
|
+
export function extractPythonAst(files) {
|
|
15
|
+
if (files.length === 0) {
|
|
16
|
+
return { endpoints: [], models: [], tasks: [], endpoint_model_usage: [], enums: [], constants: [] };
|
|
17
|
+
}
|
|
18
|
+
const moduleToFile = buildPythonModuleFileMap(files);
|
|
19
|
+
const parser = new Parser();
|
|
20
|
+
parser.setLanguage(Python);
|
|
21
|
+
const parsedFiles = [];
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
try {
|
|
24
|
+
const source = fs.readFileSync(file, "utf8");
|
|
25
|
+
let tree;
|
|
26
|
+
try {
|
|
27
|
+
tree = parser.parse(source);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
parsedFiles.push({ file, source, root: tree.rootNode });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const allModels = [];
|
|
39
|
+
const modelNames = new Set();
|
|
40
|
+
for (const parsed of parsedFiles) {
|
|
41
|
+
const { djangoModelsAliases, djangoModelClasses } = collectDjangoImports(parsed.root, parsed.source);
|
|
42
|
+
const models = extractModelsFromTree(parsed, djangoModelsAliases, djangoModelClasses);
|
|
43
|
+
for (const model of models) {
|
|
44
|
+
if (modelNames.has(model.name)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
modelNames.add(model.name);
|
|
48
|
+
allModels.push(model);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const endpoints = [];
|
|
52
|
+
const tasks = [];
|
|
53
|
+
const endpointModelUsage = [];
|
|
54
|
+
const enums = [];
|
|
55
|
+
const constants = [];
|
|
56
|
+
const includeRouterPrefixes = new Map();
|
|
57
|
+
for (const parsed of parsedFiles) {
|
|
58
|
+
const { djangoModelsAliases, djangoModelClasses } = collectDjangoImports(parsed.root, parsed.source);
|
|
59
|
+
const routerPrefixes = collectRouterPrefixes(parsed.root, parsed.source);
|
|
60
|
+
const fileEndpoints = collectDecoratedEndpoints(parsed, routerPrefixes);
|
|
61
|
+
endpoints.push(...fileEndpoints);
|
|
62
|
+
const fileTasks = collectDecoratedTasks(parsed);
|
|
63
|
+
tasks.push(...fileTasks);
|
|
64
|
+
const backgroundTasks = collectBackgroundTasks(parsed);
|
|
65
|
+
tasks.push(...backgroundTasks);
|
|
66
|
+
const importAliases = collectPythonImportAliases(parsed.root, parsed.source);
|
|
67
|
+
const fileIncludePrefixes = collectIncludeRouterPrefixes(parsed, importAliases, moduleToFile);
|
|
68
|
+
for (const [targetFile, prefixes] of fileIncludePrefixes.entries()) {
|
|
69
|
+
const existing = includeRouterPrefixes.get(targetFile) ?? [];
|
|
70
|
+
existing.push(...prefixes);
|
|
71
|
+
includeRouterPrefixes.set(targetFile, existing);
|
|
72
|
+
}
|
|
73
|
+
const urlEndpoints = collectUrlPatternEndpoints(parsed);
|
|
74
|
+
endpoints.push(...urlEndpoints);
|
|
75
|
+
const fileModelAliases = collectModelAliases(parsed.root, parsed.source, modelNames);
|
|
76
|
+
const modelUsage = collectEndpointModelUsage(parsed, modelNames, fileModelAliases);
|
|
77
|
+
endpointModelUsage.push(...modelUsage);
|
|
78
|
+
const fileEnumsAndConstants = collectEnumsAndConstants(parsed);
|
|
79
|
+
enums.push(...fileEnumsAndConstants.enums);
|
|
80
|
+
constants.push(...fileEnumsAndConstants.constants);
|
|
81
|
+
const models = extractModelsFromTree(parsed, djangoModelsAliases, djangoModelClasses);
|
|
82
|
+
for (const model of models) {
|
|
83
|
+
if (modelNames.has(model.name)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
modelNames.add(model.name);
|
|
87
|
+
allModels.push(model);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const endpoint of endpoints) {
|
|
91
|
+
const prefixes = includeRouterPrefixes.get(endpoint.file);
|
|
92
|
+
if (!prefixes || prefixes.length === 0) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
endpoint.path = applyIncludeRouterPrefix(endpoint.path, prefixes);
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
endpoints,
|
|
99
|
+
models: allModels,
|
|
100
|
+
tasks,
|
|
101
|
+
endpoint_model_usage: endpointModelUsage,
|
|
102
|
+
enums,
|
|
103
|
+
constants
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function buildPythonModuleFileMap(files) {
|
|
107
|
+
const root = findCommonDirectory(files);
|
|
108
|
+
const moduleToFile = new Map();
|
|
109
|
+
for (const file of files) {
|
|
110
|
+
const relative = path.relative(root, file).replace(/\\/g, "/");
|
|
111
|
+
if (!relative || relative.startsWith("../") || !relative.endsWith(".py")) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const withoutExt = relative.replace(/\.py$/, "");
|
|
115
|
+
const moduleName = withoutExt.endsWith("/__init__")
|
|
116
|
+
? withoutExt.slice(0, -"/__init__".length)
|
|
117
|
+
: withoutExt;
|
|
118
|
+
if (!moduleName) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
moduleToFile.set(moduleName.split("/").join("."), file);
|
|
122
|
+
}
|
|
123
|
+
return moduleToFile;
|
|
124
|
+
}
|
|
125
|
+
function findCommonDirectory(files) {
|
|
126
|
+
const [first, ...rest] = files.map((file) => path.resolve(file));
|
|
127
|
+
const segments = first.split(path.sep);
|
|
128
|
+
let shared = segments;
|
|
129
|
+
for (const current of rest) {
|
|
130
|
+
const currentSegments = current.split(path.sep);
|
|
131
|
+
let index = 0;
|
|
132
|
+
while (index < shared.length &&
|
|
133
|
+
index < currentSegments.length &&
|
|
134
|
+
shared[index] === currentSegments[index]) {
|
|
135
|
+
index += 1;
|
|
136
|
+
}
|
|
137
|
+
shared = shared.slice(0, index);
|
|
138
|
+
}
|
|
139
|
+
return shared.join(path.sep) || path.sep;
|
|
140
|
+
}
|
|
141
|
+
function collectDjangoImports(root, source) {
|
|
142
|
+
const djangoModelsAliases = new Set();
|
|
143
|
+
const djangoModelClasses = new Set();
|
|
144
|
+
walk(root, (node) => {
|
|
145
|
+
if (node.type !== "import_from_statement") {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const moduleNode = node.childForFieldName("module_name");
|
|
149
|
+
const moduleName = moduleNode ? nodeText(moduleNode, source) : "";
|
|
150
|
+
if (!moduleName) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (moduleName.startsWith("django.db")) {
|
|
154
|
+
const names = collectImportNames(node, source);
|
|
155
|
+
for (const entry of names) {
|
|
156
|
+
if (entry.name === "models") {
|
|
157
|
+
djangoModelsAliases.add(entry.alias ?? entry.name);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (moduleName.startsWith("django.db.models")) {
|
|
162
|
+
const names = collectImportNames(node, source);
|
|
163
|
+
for (const entry of names) {
|
|
164
|
+
if (entry.name === "Model") {
|
|
165
|
+
djangoModelClasses.add(entry.alias ?? entry.name);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return { djangoModelsAliases, djangoModelClasses };
|
|
171
|
+
}
|
|
172
|
+
function extractModelsFromTree(parsed, djangoModelsAliases, djangoModelClasses) {
|
|
173
|
+
const models = [];
|
|
174
|
+
walk(parsed.root, (node) => {
|
|
175
|
+
if (node.type !== "class_definition") {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const nameNode = node.childForFieldName("name");
|
|
179
|
+
const className = nameNode ? nodeText(nameNode, parsed.source) : null;
|
|
180
|
+
if (!className) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const superNode = node.childForFieldName("superclasses");
|
|
184
|
+
const baseNames = superNode
|
|
185
|
+
? collectBaseNames(superNode, parsed.source)
|
|
186
|
+
: [];
|
|
187
|
+
let isDjango = false;
|
|
188
|
+
for (const base of baseNames) {
|
|
189
|
+
if (djangoModelClasses.has(base)) {
|
|
190
|
+
isDjango = true;
|
|
191
|
+
}
|
|
192
|
+
for (const alias of djangoModelsAliases) {
|
|
193
|
+
if (base === `${alias}.Model`) {
|
|
194
|
+
isDjango = true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const isSqlAlchemy = baseNames.some((base) => base === "Base" ||
|
|
199
|
+
base.endsWith(".Base") ||
|
|
200
|
+
base.endsWith("DeclarativeBase") ||
|
|
201
|
+
base.endsWith("db.Model"));
|
|
202
|
+
const isPydantic = baseNames.some((base) => base === "BaseModel" || base.endsWith(".BaseModel"));
|
|
203
|
+
if (!isDjango && !isSqlAlchemy && !isPydantic) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const fields = [];
|
|
207
|
+
const relationships = [];
|
|
208
|
+
const fieldDetails = [];
|
|
209
|
+
const bodyNode = node.childForFieldName("body");
|
|
210
|
+
if (bodyNode) {
|
|
211
|
+
for (const child of bodyNode.namedChildren) {
|
|
212
|
+
const assignment = child.type === "assignment"
|
|
213
|
+
? child
|
|
214
|
+
: child.type === "expression_statement"
|
|
215
|
+
? child.namedChildren.find((entry) => entry.type === "assignment") ?? null
|
|
216
|
+
: null;
|
|
217
|
+
if (!assignment) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const left = assignment.childForFieldName("left");
|
|
221
|
+
const right = assignment.childForFieldName("right");
|
|
222
|
+
const target = left ? extractAssignedName(left, parsed.source) : null;
|
|
223
|
+
if (!target || !right || right.type !== "call") {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const callName = exprName(right.childForFieldName("function"), parsed.source);
|
|
227
|
+
if (!callName) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (isSqlAlchemy) {
|
|
231
|
+
if (callName === "Column" || callName === "mapped_column") {
|
|
232
|
+
fields.push(target);
|
|
233
|
+
const details = extractSqlAlchemyFieldDetails(right, parsed.source);
|
|
234
|
+
fieldDetails.push({ name: target, ...details });
|
|
235
|
+
}
|
|
236
|
+
if (callName.endsWith("relationship")) {
|
|
237
|
+
relationships.push(target);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (isDjango) {
|
|
241
|
+
const isModelsCall = callName.startsWith("models.") ||
|
|
242
|
+
Array.from(djangoModelsAliases).some((alias) => callName.startsWith(`${alias}.`));
|
|
243
|
+
if (isModelsCall) {
|
|
244
|
+
fields.push(target);
|
|
245
|
+
const details = extractDjangoFieldDetails(right, parsed.source, callName);
|
|
246
|
+
fieldDetails.push({ name: target, ...details });
|
|
247
|
+
if (callName.endsWith("ForeignKey") ||
|
|
248
|
+
callName.endsWith("ManyToManyField") ||
|
|
249
|
+
callName.endsWith("OneToOneField")) {
|
|
250
|
+
relationships.push(`${target}:${callName.split(".").pop() ?? callName}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (isPydantic && bodyNode) {
|
|
257
|
+
const pydanticFields = extractPydanticFieldDetails(bodyNode, parsed.source);
|
|
258
|
+
for (const detail of pydanticFields) {
|
|
259
|
+
fields.push(detail.name);
|
|
260
|
+
fieldDetails.push(detail);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const framework = isPydantic ? "pydantic" : isDjango && !isSqlAlchemy ? "django" : "sqlalchemy";
|
|
264
|
+
models.push({
|
|
265
|
+
name: className,
|
|
266
|
+
file: parsed.file,
|
|
267
|
+
framework,
|
|
268
|
+
fields: Array.from(new Set(fields)).sort(),
|
|
269
|
+
relationships: Array.from(new Set(relationships)).sort(),
|
|
270
|
+
field_details: fieldDetails.length > 0 ? fieldDetails : undefined
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
return models;
|
|
274
|
+
}
|
|
275
|
+
function extractPydanticFieldDetails(bodyNode, source) {
|
|
276
|
+
const details = [];
|
|
277
|
+
for (const child of bodyNode.namedChildren) {
|
|
278
|
+
if (child.type !== "expression_statement") {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const assignment = child.namedChildren.find((entry) => entry.type === "assignment") ?? child;
|
|
282
|
+
const line = source.slice(assignment.startIndex, assignment.endIndex).trim();
|
|
283
|
+
if (!line) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([^=#]+?)(?:\s*=\s*(.+))?$/);
|
|
287
|
+
if (!match) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const [, name, typeText, defaultText] = match;
|
|
291
|
+
details.push({
|
|
292
|
+
name,
|
|
293
|
+
type: typeText.trim(),
|
|
294
|
+
nullable: typeText.includes("Optional[") ||
|
|
295
|
+
typeText.includes("| None") ||
|
|
296
|
+
typeText.includes("None |"),
|
|
297
|
+
default: defaultText?.trim() ?? null
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (details.length > 0) {
|
|
301
|
+
return details;
|
|
302
|
+
}
|
|
303
|
+
const blockText = source.slice(bodyNode.startIndex, bodyNode.endIndex);
|
|
304
|
+
for (const rawLine of blockText.split(/\r?\n/)) {
|
|
305
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
306
|
+
if (!line || line.startsWith("#")) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (line.startsWith("def ") ||
|
|
310
|
+
line.startsWith("class ") ||
|
|
311
|
+
line.startsWith("@") ||
|
|
312
|
+
line.startsWith("return ") ||
|
|
313
|
+
line.startsWith("if ") ||
|
|
314
|
+
line.startsWith("for ")) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([^=#]+?)(?:\s*=\s*(.+))?$/);
|
|
318
|
+
if (!match) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const [, name, typeText, defaultText] = match;
|
|
322
|
+
details.push({
|
|
323
|
+
name,
|
|
324
|
+
type: typeText.trim(),
|
|
325
|
+
nullable: typeText.includes("Optional[") ||
|
|
326
|
+
typeText.includes("| None") ||
|
|
327
|
+
typeText.includes("None |"),
|
|
328
|
+
default: defaultText?.trim() ?? null
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return details;
|
|
332
|
+
}
|
|
333
|
+
function collectRouterPrefixes(root, source) {
|
|
334
|
+
const prefixes = new Map();
|
|
335
|
+
walk(root, (node) => {
|
|
336
|
+
if (node.type !== "assignment") {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const left = node.childForFieldName("left");
|
|
340
|
+
const right = node.childForFieldName("right");
|
|
341
|
+
if (!left || !right || right.type !== "call") {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const callName = exprName(right.childForFieldName("function"), source);
|
|
345
|
+
if (!callName || !callName.endsWith("APIRouter")) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const { keyword } = collectCallArguments(right, source);
|
|
349
|
+
const prefixNode = keyword.get("prefix");
|
|
350
|
+
const prefix = prefixNode ? stringLiteralValue(prefixNode, source) : null;
|
|
351
|
+
const names = extractAssignedNames(left, source);
|
|
352
|
+
for (const name of names) {
|
|
353
|
+
prefixes.set(name, prefix ?? "");
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
return prefixes;
|
|
357
|
+
}
|
|
358
|
+
function collectDecoratedEndpoints(parsed, routerPrefixes) {
|
|
359
|
+
const endpoints = [];
|
|
360
|
+
walk(parsed.root, (node) => {
|
|
361
|
+
if (node.type !== "decorated_definition") {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const definition = node.childForFieldName("definition");
|
|
365
|
+
if (!definition || definition.type !== "function_definition") {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const handlerNode = definition.childForFieldName("name");
|
|
369
|
+
const handler = handlerNode ? nodeText(handlerNode, parsed.source) : null;
|
|
370
|
+
if (!handler) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
for (const decoratorNode of node.namedChildren) {
|
|
374
|
+
if (decoratorNode.type !== "decorator") {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const expr = decoratorNode.namedChildren[0];
|
|
378
|
+
if (!expr || expr.type !== "call") {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const funcNode = expr.childForFieldName("function");
|
|
382
|
+
if (!funcNode || funcNode.type !== "attribute") {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
const attrNode = funcNode.childForFieldName("attribute");
|
|
386
|
+
const baseNode = funcNode.childForFieldName("object");
|
|
387
|
+
const attr = attrNode ? nodeText(attrNode, parsed.source) : "";
|
|
388
|
+
const baseName = exprName(baseNode, parsed.source);
|
|
389
|
+
if (HTTP_METHODS.has(attr)) {
|
|
390
|
+
const { positional, keyword } = collectCallArguments(expr, parsed.source);
|
|
391
|
+
const pathNode = positional[0];
|
|
392
|
+
const pathValue = pathNode ? stringLiteralValue(pathNode, parsed.source) : null;
|
|
393
|
+
if (!pathValue) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
const prefix = routerPrefixes.get(baseName ?? "") ?? "";
|
|
397
|
+
let responseSchema = null;
|
|
398
|
+
const responseModelNode = keyword.get("response_model");
|
|
399
|
+
if (responseModelNode) {
|
|
400
|
+
responseSchema = exprName(responseModelNode, parsed.source);
|
|
401
|
+
}
|
|
402
|
+
let requestSchema = null;
|
|
403
|
+
const paramsNode = definition.childForFieldName("parameters");
|
|
404
|
+
if (paramsNode) {
|
|
405
|
+
for (const param of paramsNode.namedChildren) {
|
|
406
|
+
if (param.type === "typed_parameter" || param.type === "typed_default_parameter") {
|
|
407
|
+
const typeNode = param.childForFieldName("type");
|
|
408
|
+
if (typeNode) {
|
|
409
|
+
const typeName = exprName(typeNode, parsed.source);
|
|
410
|
+
if (typeName && typeName !== "Request" && typeName !== "Response" && typeName !== "Session" && typeName !== "Depends") {
|
|
411
|
+
requestSchema = typeName;
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (!requestSchema) {
|
|
418
|
+
requestSchema = extractRequestSchemaFromParameters(paramsNode, parsed.source);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
let serviceCalls = [];
|
|
422
|
+
let aiOperations = [];
|
|
423
|
+
const bodyNode = definition.childForFieldName("body");
|
|
424
|
+
if (bodyNode) {
|
|
425
|
+
walk(bodyNode, (childNode) => {
|
|
426
|
+
if (childNode.type === "call") {
|
|
427
|
+
const callName = exprName(childNode.childForFieldName("function"), parsed.source);
|
|
428
|
+
if (callName && callName !== "Depends") {
|
|
429
|
+
serviceCalls.push(callName);
|
|
430
|
+
const lowerCall = callName.toLowerCase();
|
|
431
|
+
if (lowerCall.includes("openai") || lowerCall.includes("chatcompletions") || lowerCall.includes("anthropic")) {
|
|
432
|
+
const { keyword } = collectCallArguments(childNode, parsed.source);
|
|
433
|
+
const modelNode = keyword.get("model");
|
|
434
|
+
const model = modelNode ? stringLiteralValue(modelNode, parsed.source) : null;
|
|
435
|
+
const tokenBudget = extractTokenBudget(keyword, parsed.source);
|
|
436
|
+
aiOperations.push({
|
|
437
|
+
provider: lowerCall.includes("openai") ? "openai" : "anthropic",
|
|
438
|
+
operation: callName,
|
|
439
|
+
model,
|
|
440
|
+
max_tokens: tokenBudget.maxTokens,
|
|
441
|
+
max_output_tokens: tokenBudget.maxOutputTokens,
|
|
442
|
+
token_budget: tokenBudget.tokenBudget
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
endpoints.push({
|
|
450
|
+
file: parsed.file,
|
|
451
|
+
method: attr.toUpperCase(),
|
|
452
|
+
path: normalizePath(prefix, pathValue),
|
|
453
|
+
handler,
|
|
454
|
+
router: baseName,
|
|
455
|
+
request_schema: requestSchema,
|
|
456
|
+
response_schema: responseSchema,
|
|
457
|
+
service_calls: Array.from(new Set(serviceCalls)).sort(),
|
|
458
|
+
ai_operations: aiOperations
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
else if (attr === "api_route") {
|
|
462
|
+
const { positional, keyword } = collectCallArguments(expr, parsed.source);
|
|
463
|
+
const pathNode = positional[0];
|
|
464
|
+
const pathValue = pathNode ? stringLiteralValue(pathNode, parsed.source) : null;
|
|
465
|
+
if (!pathValue) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const methodsNode = keyword.get("methods") ?? positional[1];
|
|
469
|
+
const methods = methodsNode ? collectStringList(methodsNode, parsed.source) : [];
|
|
470
|
+
const prefix = routerPrefixes.get(baseName ?? "") ?? "";
|
|
471
|
+
const methodList = methods.length > 0 ? methods : ["ANY"];
|
|
472
|
+
let responseSchema = null;
|
|
473
|
+
const responseModelNode = keyword.get("response_model");
|
|
474
|
+
if (responseModelNode) {
|
|
475
|
+
responseSchema = exprName(responseModelNode, parsed.source);
|
|
476
|
+
}
|
|
477
|
+
let requestSchema = null;
|
|
478
|
+
const paramsNode = definition.childForFieldName("parameters");
|
|
479
|
+
if (paramsNode) {
|
|
480
|
+
for (const param of paramsNode.namedChildren) {
|
|
481
|
+
if (param.type === "typed_parameter" || param.type === "typed_default_parameter") {
|
|
482
|
+
const typeNode = param.childForFieldName("type");
|
|
483
|
+
if (typeNode) {
|
|
484
|
+
const typeName = exprName(typeNode, parsed.source);
|
|
485
|
+
if (typeName && typeName !== "Request" && typeName !== "Response" && typeName !== "Session" && typeName !== "Depends") {
|
|
486
|
+
requestSchema = typeName;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (!requestSchema) {
|
|
493
|
+
requestSchema = extractRequestSchemaFromParameters(paramsNode, parsed.source);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
let serviceCalls = [];
|
|
497
|
+
let aiOperations = [];
|
|
498
|
+
const bodyNode = definition.childForFieldName("body");
|
|
499
|
+
if (bodyNode) {
|
|
500
|
+
walk(bodyNode, (childNode) => {
|
|
501
|
+
if (childNode.type === "call") {
|
|
502
|
+
const callName = exprName(childNode.childForFieldName("function"), parsed.source);
|
|
503
|
+
if (callName && callName !== "Depends") {
|
|
504
|
+
serviceCalls.push(callName);
|
|
505
|
+
const lowerCall = callName.toLowerCase();
|
|
506
|
+
if (lowerCall.includes("openai") || lowerCall.includes("chatcompletions") || lowerCall.includes("anthropic")) {
|
|
507
|
+
const { keyword } = collectCallArguments(childNode, parsed.source);
|
|
508
|
+
const modelNode = keyword.get("model");
|
|
509
|
+
const model = modelNode ? stringLiteralValue(modelNode, parsed.source) : null;
|
|
510
|
+
const tokenBudget = extractTokenBudget(keyword, parsed.source);
|
|
511
|
+
aiOperations.push({
|
|
512
|
+
provider: lowerCall.includes("openai") ? "openai" : "anthropic",
|
|
513
|
+
operation: callName,
|
|
514
|
+
model,
|
|
515
|
+
max_tokens: tokenBudget.maxTokens,
|
|
516
|
+
max_output_tokens: tokenBudget.maxOutputTokens,
|
|
517
|
+
token_budget: tokenBudget.tokenBudget
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
for (const method of methodList) {
|
|
525
|
+
endpoints.push({
|
|
526
|
+
file: parsed.file,
|
|
527
|
+
method: method.toUpperCase(),
|
|
528
|
+
path: normalizePath(prefix, pathValue),
|
|
529
|
+
handler,
|
|
530
|
+
router: baseName,
|
|
531
|
+
request_schema: requestSchema,
|
|
532
|
+
response_schema: responseSchema,
|
|
533
|
+
service_calls: Array.from(new Set(serviceCalls)).sort(),
|
|
534
|
+
ai_operations: aiOperations
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
return endpoints;
|
|
541
|
+
}
|
|
542
|
+
function extractRequestSchemaFromParameters(paramsNode, source) {
|
|
543
|
+
const raw = source.slice(paramsNode.startIndex, paramsNode.endIndex);
|
|
544
|
+
const matches = raw.matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*:\s*([^=,\)\n]+)/g);
|
|
545
|
+
const blocked = new Set([
|
|
546
|
+
"Request",
|
|
547
|
+
"Response",
|
|
548
|
+
"Session",
|
|
549
|
+
"Depends",
|
|
550
|
+
"BackgroundTasks",
|
|
551
|
+
"UploadFile",
|
|
552
|
+
"File",
|
|
553
|
+
"Form",
|
|
554
|
+
"Query",
|
|
555
|
+
"Path",
|
|
556
|
+
"Body",
|
|
557
|
+
"str",
|
|
558
|
+
"int",
|
|
559
|
+
"float",
|
|
560
|
+
"bool",
|
|
561
|
+
"dict",
|
|
562
|
+
"list",
|
|
563
|
+
"Dict",
|
|
564
|
+
"List",
|
|
565
|
+
"Optional",
|
|
566
|
+
"Any"
|
|
567
|
+
]);
|
|
568
|
+
for (const match of matches) {
|
|
569
|
+
const paramName = match[1]?.trim() ?? "";
|
|
570
|
+
const typeText = match[2]?.trim() ?? "";
|
|
571
|
+
if (paramName === "self" || paramName === "cls" || /(^id$|_id$)/i.test(paramName)) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const cleaned = typeText
|
|
575
|
+
.replace(/\s+/g, "")
|
|
576
|
+
.replace(/^Annotated\[/, "")
|
|
577
|
+
.split(/[,\[\]|]/)[0]
|
|
578
|
+
.trim();
|
|
579
|
+
if (!cleaned || blocked.has(cleaned)) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
return cleaned.split(".").pop() ?? cleaned;
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
function collectDecoratedTasks(parsed) {
|
|
587
|
+
const tasks = [];
|
|
588
|
+
walk(parsed.root, (node) => {
|
|
589
|
+
if (node.type !== "decorated_definition") {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const definition = node.childForFieldName("definition");
|
|
593
|
+
if (!definition || definition.type !== "function_definition") {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const handlerNode = definition.childForFieldName("name");
|
|
597
|
+
const handler = handlerNode ? nodeText(handlerNode, parsed.source) : "anonymous";
|
|
598
|
+
for (const decoratorNode of node.namedChildren) {
|
|
599
|
+
if (decoratorNode.type !== "decorator") {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const expr = decoratorNode.namedChildren[0];
|
|
603
|
+
if (!expr || expr.type !== "call") {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
const funcName = exprName(expr.childForFieldName("function"), parsed.source) ?? "";
|
|
607
|
+
if (!funcName.endsWith("shared_task") && !funcName.endsWith("task")) {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const { keyword } = collectCallArguments(expr, parsed.source);
|
|
611
|
+
const nameNode = keyword.get("name");
|
|
612
|
+
const queueNode = keyword.get("queue");
|
|
613
|
+
const taskName = nameNode ? stringLiteralValue(nameNode, parsed.source) : null;
|
|
614
|
+
const queue = queueNode ? stringLiteralValue(queueNode, parsed.source) : null;
|
|
615
|
+
tasks.push({
|
|
616
|
+
name: taskName ?? handler,
|
|
617
|
+
file: parsed.file,
|
|
618
|
+
kind: "celery",
|
|
619
|
+
queue: queue ?? undefined,
|
|
620
|
+
schedule: null
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
return tasks;
|
|
625
|
+
}
|
|
626
|
+
function collectBackgroundTasks(parsed) {
|
|
627
|
+
const tasks = [];
|
|
628
|
+
walk(parsed.root, (node) => {
|
|
629
|
+
if (node.type !== "call") {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const funcName = exprName(node.childForFieldName("function"), parsed.source) ?? "";
|
|
633
|
+
if (!funcName.endsWith("add_task")) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const { positional } = collectCallArguments(node, parsed.source);
|
|
637
|
+
const taskExpr = positional[0];
|
|
638
|
+
const taskName = taskExpr ? exprName(taskExpr, parsed.source) ?? "anonymous" : "anonymous";
|
|
639
|
+
tasks.push({
|
|
640
|
+
name: taskName,
|
|
641
|
+
file: parsed.file,
|
|
642
|
+
kind: "background",
|
|
643
|
+
queue: null,
|
|
644
|
+
schedule: null
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
return tasks;
|
|
648
|
+
}
|
|
649
|
+
function collectPythonImportAliases(root, source) {
|
|
650
|
+
const aliases = new Map();
|
|
651
|
+
walk(root, (node) => {
|
|
652
|
+
if (node.type !== "import_from_statement" && node.type !== "import_statement") {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const raw = nodeText(node, source)
|
|
656
|
+
.replace(/#[^\n]*/g, "")
|
|
657
|
+
.replace(/\s+/g, " ")
|
|
658
|
+
.trim();
|
|
659
|
+
if (node.type === "import_from_statement") {
|
|
660
|
+
const match = raw.match(/^from\s+([A-Za-z0-9_.]+)\s+import\s+(.+)$/);
|
|
661
|
+
if (!match) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const modulePath = match[1];
|
|
665
|
+
const importList = match[2].replace(/[()]/g, "");
|
|
666
|
+
for (const part of importList.split(",").map((entry) => entry.trim()).filter(Boolean)) {
|
|
667
|
+
const aliasMatch = part.match(/^([A-Za-z0-9_]+)(?:\s+as\s+([A-Za-z0-9_]+))?$/);
|
|
668
|
+
if (!aliasMatch) {
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
const original = aliasMatch[1];
|
|
672
|
+
const alias = aliasMatch[2] ?? original;
|
|
673
|
+
aliases.set(alias, `${modulePath}.${original}`);
|
|
674
|
+
}
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const match = raw.match(/^import\s+(.+)$/);
|
|
678
|
+
if (!match) {
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
for (const part of match[1].split(",").map((entry) => entry.trim()).filter(Boolean)) {
|
|
682
|
+
const aliasMatch = part.match(/^([A-Za-z0-9_.]+)(?:\s+as\s+([A-Za-z0-9_]+))?$/);
|
|
683
|
+
if (!aliasMatch) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const original = aliasMatch[1];
|
|
687
|
+
const alias = aliasMatch[2] ?? original.split(".").pop() ?? original;
|
|
688
|
+
aliases.set(alias, original);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
return aliases;
|
|
692
|
+
}
|
|
693
|
+
function collectIncludeRouterPrefixes(parsed, importAliases, moduleToFile) {
|
|
694
|
+
const prefixes = new Map();
|
|
695
|
+
walk(parsed.root, (node) => {
|
|
696
|
+
if (node.type !== "call") {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const funcName = exprName(node.childForFieldName("function"), parsed.source) ?? "";
|
|
700
|
+
if (!funcName.endsWith("include_router")) {
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const { positional, keyword } = collectCallArguments(node, parsed.source);
|
|
704
|
+
const routerNode = positional[0];
|
|
705
|
+
const routerName = routerNode ? exprName(routerNode, parsed.source) : null;
|
|
706
|
+
if (!routerName) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const targetFile = resolveImportedTargetFile(routerName, importAliases, moduleToFile);
|
|
710
|
+
if (!targetFile) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const prefixNode = keyword.get("prefix");
|
|
714
|
+
const prefix = prefixNode ? stringLiteralValue(prefixNode, parsed.source) : "";
|
|
715
|
+
const list = prefixes.get(targetFile) ?? [];
|
|
716
|
+
list.push(prefix ?? "");
|
|
717
|
+
prefixes.set(targetFile, list);
|
|
718
|
+
});
|
|
719
|
+
return prefixes;
|
|
720
|
+
}
|
|
721
|
+
function resolveImportedTargetFile(target, importAliases, moduleToFile) {
|
|
722
|
+
const segments = target.split(".").filter(Boolean);
|
|
723
|
+
if (segments.length === 0) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const [first, ...rest] = segments;
|
|
727
|
+
const resolvedBase = importAliases.get(first) ?? first;
|
|
728
|
+
let candidate = rest.length > 0 ? `${resolvedBase}.${rest.join(".")}` : resolvedBase;
|
|
729
|
+
while (candidate) {
|
|
730
|
+
const resolved = moduleToFile.get(candidate);
|
|
731
|
+
if (resolved) {
|
|
732
|
+
return resolved;
|
|
733
|
+
}
|
|
734
|
+
if (!candidate.includes(".")) {
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
candidate = candidate.slice(0, candidate.lastIndexOf("."));
|
|
738
|
+
}
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
function applyIncludeRouterPrefix(route, prefixes) {
|
|
742
|
+
const normalizedRoute = normalizePath("", route);
|
|
743
|
+
const chosenPrefix = prefixes
|
|
744
|
+
.filter((prefix) => typeof prefix === "string")
|
|
745
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
746
|
+
if (!chosenPrefix) {
|
|
747
|
+
return normalizedRoute;
|
|
748
|
+
}
|
|
749
|
+
const normalizedPrefix = normalizePath("", chosenPrefix);
|
|
750
|
+
if (normalizedRoute === normalizedPrefix ||
|
|
751
|
+
normalizedRoute.startsWith(`${normalizedPrefix}/`)) {
|
|
752
|
+
return normalizedRoute;
|
|
753
|
+
}
|
|
754
|
+
return normalizePath(normalizedPrefix, normalizedRoute);
|
|
755
|
+
}
|
|
756
|
+
function collectUrlPatternEndpoints(parsed) {
|
|
757
|
+
const endpoints = [];
|
|
758
|
+
walk(parsed.root, (node) => {
|
|
759
|
+
if (node.type !== "assignment") {
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const left = node.childForFieldName("left");
|
|
763
|
+
const right = node.childForFieldName("right");
|
|
764
|
+
if (!left || !right || right.type !== "list") {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const names = extractAssignedNames(left, parsed.source);
|
|
768
|
+
if (!names.includes("urlpatterns")) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
for (const element of right.namedChildren) {
|
|
772
|
+
if (element.type !== "call") {
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const funcName = exprName(element.childForFieldName("function"), parsed.source);
|
|
776
|
+
const lastSegment = funcName ? funcName.split(".").pop() ?? funcName : "";
|
|
777
|
+
if (lastSegment !== "path" && lastSegment !== "re_path" && lastSegment !== "url") {
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const { positional } = collectCallArguments(element, parsed.source);
|
|
781
|
+
const pathNode = positional[0];
|
|
782
|
+
const handlerNode = positional[1];
|
|
783
|
+
const pathValue = pathNode ? stringLiteralValue(pathNode, parsed.source) : null;
|
|
784
|
+
const handler = handlerNode ? exprName(handlerNode, parsed.source) : null;
|
|
785
|
+
if (!pathValue || !handler) {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
endpoints.push({
|
|
789
|
+
file: parsed.file,
|
|
790
|
+
method: "ANY",
|
|
791
|
+
path: normalizePath("", pathValue),
|
|
792
|
+
handler,
|
|
793
|
+
router: null,
|
|
794
|
+
service_calls: [],
|
|
795
|
+
ai_operations: []
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
return endpoints;
|
|
800
|
+
}
|
|
801
|
+
function collectEnumsAndConstants(parsed) {
|
|
802
|
+
const enums = [];
|
|
803
|
+
const constants = [];
|
|
804
|
+
for (const node of parsed.root.namedChildren) {
|
|
805
|
+
if (node.type === "class_definition") {
|
|
806
|
+
const nameNode = node.childForFieldName("name");
|
|
807
|
+
const className = nameNode ? nodeText(nameNode, parsed.source) : null;
|
|
808
|
+
if (!className)
|
|
809
|
+
continue;
|
|
810
|
+
const superNode = node.childForFieldName("superclasses");
|
|
811
|
+
const baseNames = superNode ? collectBaseNames(superNode, parsed.source) : [];
|
|
812
|
+
const isEnum = baseNames.some(base => base === "Enum" || base.endsWith(".Enum"));
|
|
813
|
+
if (isEnum) {
|
|
814
|
+
const values = [];
|
|
815
|
+
const bodyNode = node.childForFieldName("body");
|
|
816
|
+
if (bodyNode) {
|
|
817
|
+
for (const child of bodyNode.namedChildren) {
|
|
818
|
+
if (child.type === "assignment") {
|
|
819
|
+
const target = child.childForFieldName("left");
|
|
820
|
+
if (target) {
|
|
821
|
+
values.push(nodeText(target, parsed.source));
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
enums.push({ name: className, file: parsed.file, values });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
else if (node.type === "assignment" || node.type === "expression_statement") {
|
|
830
|
+
// Look for module-level constants (ALL_CAPS)
|
|
831
|
+
// `MAX_RETRIES = 5` (assignment)
|
|
832
|
+
// `MAX_RETRIES: int = 5` (expression_statement -> assignment)
|
|
833
|
+
let assignmentNode = node;
|
|
834
|
+
if (node.type === "expression_statement") {
|
|
835
|
+
const firstChild = node.namedChildren[0];
|
|
836
|
+
if (firstChild && firstChild.type === "assignment") {
|
|
837
|
+
assignmentNode = firstChild;
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const left = assignmentNode.childForFieldName("left");
|
|
844
|
+
const typeNode = assignmentNode.childForFieldName("type");
|
|
845
|
+
const right = assignmentNode.childForFieldName("right");
|
|
846
|
+
if (!left || !right)
|
|
847
|
+
continue;
|
|
848
|
+
const targetName = extractAssignedName(left, parsed.source);
|
|
849
|
+
if (targetName && targetName === targetName.toUpperCase() && targetName !== "_") {
|
|
850
|
+
constants.push({
|
|
851
|
+
name: targetName,
|
|
852
|
+
file: parsed.file,
|
|
853
|
+
type: typeNode ? nodeText(typeNode, parsed.source) : "unknown",
|
|
854
|
+
value: nodeText(right, parsed.source)
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return { enums, constants };
|
|
860
|
+
}
|
|
861
|
+
function collectEndpointModelUsage(parsed, modelNames, aliases) {
|
|
862
|
+
const usage = [];
|
|
863
|
+
if (modelNames.size === 0) {
|
|
864
|
+
return usage;
|
|
865
|
+
}
|
|
866
|
+
walk(parsed.root, (node) => {
|
|
867
|
+
if (node.type !== "function_definition") {
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const nameNode = node.childForFieldName("name");
|
|
871
|
+
const handler = nameNode ? nodeText(nameNode, parsed.source) : null;
|
|
872
|
+
if (!handler) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
const bodyNode = node.childForFieldName("body");
|
|
876
|
+
if (!bodyNode) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const used = collectModelsUsed(bodyNode, parsed.source, modelNames, aliases);
|
|
880
|
+
if (used.size > 0) {
|
|
881
|
+
usage.push({
|
|
882
|
+
file: parsed.file,
|
|
883
|
+
handler,
|
|
884
|
+
models: Array.from(used).sort()
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
return usage;
|
|
889
|
+
}
|
|
890
|
+
function collectModelAliases(root, source, modelNames) {
|
|
891
|
+
const aliases = new Map();
|
|
892
|
+
if (modelNames.size === 0) {
|
|
893
|
+
return aliases;
|
|
894
|
+
}
|
|
895
|
+
walk(root, (node) => {
|
|
896
|
+
if (node.type === "import_statement") {
|
|
897
|
+
const entries = collectImportNames(node, source);
|
|
898
|
+
for (const entry of entries) {
|
|
899
|
+
const name = entry.name.split(".").pop() ?? entry.name;
|
|
900
|
+
if (modelNames.has(name)) {
|
|
901
|
+
aliases.set(entry.alias ?? name, name);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (node.type === "import_from_statement") {
|
|
906
|
+
const entries = collectImportNames(node, source);
|
|
907
|
+
for (const entry of entries) {
|
|
908
|
+
const name = entry.name.split(".").pop() ?? entry.name;
|
|
909
|
+
if (modelNames.has(name)) {
|
|
910
|
+
aliases.set(entry.alias ?? name, name);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
return aliases;
|
|
916
|
+
}
|
|
917
|
+
function collectImportNames(node, source) {
|
|
918
|
+
const entries = [];
|
|
919
|
+
for (const child of node.namedChildren) {
|
|
920
|
+
if (child.type === "aliased_import") {
|
|
921
|
+
const nameNode = child.childForFieldName("name");
|
|
922
|
+
const aliasNode = child.childForFieldName("alias");
|
|
923
|
+
if (nameNode) {
|
|
924
|
+
entries.push({
|
|
925
|
+
name: nodeText(nameNode, source),
|
|
926
|
+
alias: aliasNode ? nodeText(aliasNode, source) : undefined
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
else if (child.type === "dotted_name" || child.type === "identifier") {
|
|
931
|
+
entries.push({ name: nodeText(child, source) });
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return entries;
|
|
935
|
+
}
|
|
936
|
+
function collectCallArguments(callNode, source) {
|
|
937
|
+
const positional = [];
|
|
938
|
+
const keyword = new Map();
|
|
939
|
+
const argsNode = callNode.childForFieldName("arguments");
|
|
940
|
+
if (!argsNode) {
|
|
941
|
+
return { positional, keyword };
|
|
942
|
+
}
|
|
943
|
+
for (const arg of argsNode.namedChildren) {
|
|
944
|
+
if (arg.type === "keyword_argument") {
|
|
945
|
+
const nameNode = arg.childForFieldName("name");
|
|
946
|
+
const valueNode = arg.childForFieldName("value");
|
|
947
|
+
if (nameNode && valueNode) {
|
|
948
|
+
keyword.set(nodeText(nameNode, source), valueNode);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
else {
|
|
952
|
+
positional.push(arg);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return { positional, keyword };
|
|
956
|
+
}
|
|
957
|
+
function collectModelsUsed(root, source, modelNames, aliases) {
|
|
958
|
+
const used = new Set();
|
|
959
|
+
walk(root, (node) => {
|
|
960
|
+
if (node.type === "identifier") {
|
|
961
|
+
const name = nodeText(node, source);
|
|
962
|
+
if (modelNames.has(name)) {
|
|
963
|
+
used.add(name);
|
|
964
|
+
}
|
|
965
|
+
if (aliases.has(name)) {
|
|
966
|
+
used.add(aliases.get(name) ?? name);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
else if (node.type === "attribute") {
|
|
970
|
+
const attrNode = node.childForFieldName("attribute");
|
|
971
|
+
const attr = attrNode ? nodeText(attrNode, source) : null;
|
|
972
|
+
if (attr && modelNames.has(attr)) {
|
|
973
|
+
used.add(attr);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
return used;
|
|
978
|
+
}
|
|
979
|
+
function collectBaseNames(superNode, source) {
|
|
980
|
+
const names = [];
|
|
981
|
+
for (const child of superNode.namedChildren) {
|
|
982
|
+
if (child.type === "keyword_argument") {
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
985
|
+
const name = exprName(child, source);
|
|
986
|
+
if (name) {
|
|
987
|
+
names.push(name);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return names;
|
|
991
|
+
}
|
|
992
|
+
function collectStringList(node, source) {
|
|
993
|
+
if (node.type === "list" || node.type === "tuple") {
|
|
994
|
+
return node.namedChildren
|
|
995
|
+
.map((child) => stringLiteralValue(child, source))
|
|
996
|
+
.filter((value) => Boolean(value))
|
|
997
|
+
.map((value) => value.toUpperCase());
|
|
998
|
+
}
|
|
999
|
+
const single = stringLiteralValue(node, source);
|
|
1000
|
+
return single ? [single.toUpperCase()] : [];
|
|
1001
|
+
}
|
|
1002
|
+
function extractAssignedNames(node, source) {
|
|
1003
|
+
if (node.type === "identifier") {
|
|
1004
|
+
return [nodeText(node, source)];
|
|
1005
|
+
}
|
|
1006
|
+
const names = [];
|
|
1007
|
+
for (const child of node.namedChildren) {
|
|
1008
|
+
if (child.type === "identifier") {
|
|
1009
|
+
names.push(nodeText(child, source));
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return names;
|
|
1013
|
+
}
|
|
1014
|
+
function extractAssignedName(node, source) {
|
|
1015
|
+
const names = extractAssignedNames(node, source);
|
|
1016
|
+
return names[0] ?? null;
|
|
1017
|
+
}
|
|
1018
|
+
function normalizePath(prefix, route) {
|
|
1019
|
+
const cleanedPrefix = prefix || "";
|
|
1020
|
+
const cleanedRoute = route || "";
|
|
1021
|
+
const pref = cleanedPrefix && !cleanedPrefix.startsWith("/") ? `/${cleanedPrefix}` : cleanedPrefix;
|
|
1022
|
+
const rt = cleanedRoute && !cleanedRoute.startsWith("/") ? `/${cleanedRoute}` : cleanedRoute;
|
|
1023
|
+
const combined = `${pref}${rt}`.replace(/\/+/g, "/");
|
|
1024
|
+
return combined || "/";
|
|
1025
|
+
}
|
|
1026
|
+
function stringLiteralValue(node, source) {
|
|
1027
|
+
if (node.type !== "string") {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
const raw = nodeText(node, source).trim();
|
|
1031
|
+
const match = raw.match(/^([rubfRUBF]*)(['"]{1,3})([\s\S]*?)\2$/);
|
|
1032
|
+
if (!match) {
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
return match[3];
|
|
1036
|
+
}
|
|
1037
|
+
function exprName(node, source) {
|
|
1038
|
+
if (!node) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
if (node.type === "identifier") {
|
|
1042
|
+
return nodeText(node, source);
|
|
1043
|
+
}
|
|
1044
|
+
if (node.type === "attribute") {
|
|
1045
|
+
const obj = node.childForFieldName("object");
|
|
1046
|
+
const attr = node.childForFieldName("attribute");
|
|
1047
|
+
const base = exprName(obj, source);
|
|
1048
|
+
const attrName = attr ? nodeText(attr, source) : null;
|
|
1049
|
+
if (base && attrName) {
|
|
1050
|
+
return `${base}.${attrName}`;
|
|
1051
|
+
}
|
|
1052
|
+
return attrName ?? base;
|
|
1053
|
+
}
|
|
1054
|
+
if (node.type === "call") {
|
|
1055
|
+
return exprName(node.childForFieldName("function"), source);
|
|
1056
|
+
}
|
|
1057
|
+
if (node.type === "dotted_name") {
|
|
1058
|
+
return nodeText(node, source);
|
|
1059
|
+
}
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
function booleanLiteralValue(node, source) {
|
|
1063
|
+
if (!node) {
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
if (node.type === "true") {
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
if (node.type === "false") {
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
function numberLiteralValue(node, source) {
|
|
1075
|
+
if (!node) {
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
if (node.type === "integer" || node.type === "float") {
|
|
1079
|
+
const raw = nodeText(node, source);
|
|
1080
|
+
const parsed = Number(raw);
|
|
1081
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1082
|
+
}
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
function literalValue(node, source) {
|
|
1086
|
+
if (!node) {
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
const str = stringLiteralValue(node, source);
|
|
1090
|
+
if (str !== null) {
|
|
1091
|
+
return str;
|
|
1092
|
+
}
|
|
1093
|
+
if (node.type === "integer" || node.type === "float") {
|
|
1094
|
+
return nodeText(node, source);
|
|
1095
|
+
}
|
|
1096
|
+
if (node.type === "true" || node.type === "false" || node.type === "none") {
|
|
1097
|
+
return node.type;
|
|
1098
|
+
}
|
|
1099
|
+
return exprName(node, source);
|
|
1100
|
+
}
|
|
1101
|
+
function extractSqlAlchemyFieldDetails(callNode, source) {
|
|
1102
|
+
const { positional, keyword } = collectCallArguments(callNode, source);
|
|
1103
|
+
let typeName = null;
|
|
1104
|
+
let enumName = null;
|
|
1105
|
+
let foreignKey = null;
|
|
1106
|
+
for (const arg of positional) {
|
|
1107
|
+
if (arg.type === "call") {
|
|
1108
|
+
const fnName = exprName(arg.childForFieldName("function"), source) ?? "";
|
|
1109
|
+
if (fnName.endsWith("ForeignKey")) {
|
|
1110
|
+
const { positional: fkArgs } = collectCallArguments(arg, source);
|
|
1111
|
+
const fk = fkArgs[0] ? literalValue(fkArgs[0], source) : null;
|
|
1112
|
+
if (fk) {
|
|
1113
|
+
foreignKey = fk;
|
|
1114
|
+
}
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
if (fnName.endsWith("Enum") || fnName === "Enum") {
|
|
1118
|
+
typeName = "Enum";
|
|
1119
|
+
const { positional: enumArgs } = collectCallArguments(arg, source);
|
|
1120
|
+
const enumCandidate = enumArgs[0] ? exprName(enumArgs[0], source) : null;
|
|
1121
|
+
if (enumCandidate) {
|
|
1122
|
+
enumName = enumCandidate;
|
|
1123
|
+
}
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if (!typeName) {
|
|
1128
|
+
const name = exprName(arg, source);
|
|
1129
|
+
if (name) {
|
|
1130
|
+
typeName = name;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
const nullable = booleanLiteralValue(keyword.get("nullable"), source);
|
|
1135
|
+
const primaryKey = booleanLiteralValue(keyword.get("primary_key"), source);
|
|
1136
|
+
const defaultValue = literalValue(keyword.get("default"), source);
|
|
1137
|
+
return {
|
|
1138
|
+
type: typeName,
|
|
1139
|
+
nullable,
|
|
1140
|
+
primary_key: primaryKey,
|
|
1141
|
+
foreign_key: foreignKey,
|
|
1142
|
+
enum: enumName,
|
|
1143
|
+
default: defaultValue
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function extractDjangoFieldDetails(callNode, source, callName) {
|
|
1147
|
+
const { positional, keyword } = collectCallArguments(callNode, source);
|
|
1148
|
+
const type = callName.split(".").pop() ?? callName;
|
|
1149
|
+
const nullable = booleanLiteralValue(keyword.get("null"), source);
|
|
1150
|
+
const primaryKey = booleanLiteralValue(keyword.get("primary_key"), source);
|
|
1151
|
+
const defaultValue = literalValue(keyword.get("default"), source);
|
|
1152
|
+
let foreignKey = null;
|
|
1153
|
+
if (type.endsWith("ForeignKey") ||
|
|
1154
|
+
type.endsWith("OneToOneField") ||
|
|
1155
|
+
type.endsWith("ManyToManyField")) {
|
|
1156
|
+
const target = positional[0]
|
|
1157
|
+
? literalValue(positional[0], source)
|
|
1158
|
+
: literalValue(keyword.get("to"), source);
|
|
1159
|
+
if (target) {
|
|
1160
|
+
foreignKey = target;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
let enumName = null;
|
|
1164
|
+
const choicesNode = keyword.get("choices");
|
|
1165
|
+
if (choicesNode) {
|
|
1166
|
+
const choicesExpr = exprName(choicesNode, source);
|
|
1167
|
+
if (choicesExpr && choicesExpr.endsWith(".choices")) {
|
|
1168
|
+
enumName = choicesExpr.replace(/\.choices$/, "");
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return {
|
|
1172
|
+
type,
|
|
1173
|
+
nullable,
|
|
1174
|
+
primary_key: primaryKey,
|
|
1175
|
+
foreign_key: foreignKey,
|
|
1176
|
+
enum: enumName,
|
|
1177
|
+
default: defaultValue
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
function extractTokenBudget(keyword, source) {
|
|
1181
|
+
const maxTokens = numberLiteralValue(keyword.get("max_tokens") ??
|
|
1182
|
+
keyword.get("max_completion_tokens") ??
|
|
1183
|
+
keyword.get("max_new_tokens") ??
|
|
1184
|
+
keyword.get("tokens") ??
|
|
1185
|
+
keyword.get("token_limit"), source);
|
|
1186
|
+
const maxOutputTokens = numberLiteralValue(keyword.get("max_output_tokens"), source);
|
|
1187
|
+
const tokenBudget = maxOutputTokens ?? maxTokens;
|
|
1188
|
+
return {
|
|
1189
|
+
maxTokens,
|
|
1190
|
+
maxOutputTokens,
|
|
1191
|
+
tokenBudget
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
function nodeText(node, source) {
|
|
1195
|
+
return source.slice(node.startIndex, node.endIndex);
|
|
1196
|
+
}
|
|
1197
|
+
function walk(node, visit) {
|
|
1198
|
+
visit(node);
|
|
1199
|
+
for (const child of node.namedChildren) {
|
|
1200
|
+
walk(child, visit);
|
|
1201
|
+
}
|
|
1202
|
+
}
|