@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,1704 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
import { addEdge, ensureNode, findCycles, inboundCounts } from "../graph.js";
|
|
5
|
+
import { createIgnoreMatcher } from "../ignore.js";
|
|
6
|
+
import { extractPythonAst } from "../python.js";
|
|
7
|
+
import { findDuplicateFunctions } from "../duplicates.js";
|
|
8
|
+
import { computeTestCoverage } from "../tests.js";
|
|
9
|
+
import { hashContent, loadBackendExtractionCache, saveBackendExtractionCache } from "../cache.js";
|
|
10
|
+
import { getAdapterForFile, runAdapter } from "../../adapters/index.js";
|
|
11
|
+
const CODE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py"]);
|
|
12
|
+
const JS_RESOLVE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
13
|
+
const HTTP_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head"]);
|
|
14
|
+
function toPosix(p) {
|
|
15
|
+
return p.split(path.sep).join("/");
|
|
16
|
+
}
|
|
17
|
+
function isCodeFile(filePath) {
|
|
18
|
+
return CODE_EXTENSIONS.has(path.extname(filePath));
|
|
19
|
+
}
|
|
20
|
+
async function listFiles(root, ignore, baseRoot) {
|
|
21
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
22
|
+
const files = [];
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const fullPath = path.join(root, entry.name);
|
|
25
|
+
if (entry.isDirectory()) {
|
|
26
|
+
if (ignore.isIgnoredDir(entry.name, fullPath)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
files.push(...(await listFiles(fullPath, ignore, baseRoot)));
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (entry.isFile()) {
|
|
33
|
+
const relative = path.relative(baseRoot, fullPath);
|
|
34
|
+
if (!ignore.isIgnoredPath(relative)) {
|
|
35
|
+
files.push(fullPath);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return files;
|
|
40
|
+
}
|
|
41
|
+
async function fileExists(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
const stat = await fs.stat(filePath);
|
|
44
|
+
return stat.isFile();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function resolveFileCandidate(basePath, extensions) {
|
|
51
|
+
const ext = path.extname(basePath);
|
|
52
|
+
if (ext) {
|
|
53
|
+
if (await fileExists(basePath)) {
|
|
54
|
+
return basePath;
|
|
55
|
+
}
|
|
56
|
+
const withoutExt = basePath.slice(0, -ext.length);
|
|
57
|
+
for (const replacement of extensions) {
|
|
58
|
+
const candidate = `${withoutExt}${replacement}`;
|
|
59
|
+
if (await fileExists(candidate)) {
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const replacement of extensions) {
|
|
64
|
+
const candidate = path.join(withoutExt, `index${replacement}`);
|
|
65
|
+
if (await fileExists(candidate)) {
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
for (const replacement of extensions) {
|
|
72
|
+
const candidate = `${basePath}${replacement}`;
|
|
73
|
+
if (await fileExists(candidate)) {
|
|
74
|
+
return candidate;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const replacement of extensions) {
|
|
78
|
+
const candidate = path.join(basePath, `index${replacement}`);
|
|
79
|
+
if (await fileExists(candidate)) {
|
|
80
|
+
return candidate;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
async function resolveJsImport(fromFile, specifier) {
|
|
86
|
+
if (!specifier.startsWith(".")) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const resolved = path.resolve(path.dirname(fromFile), specifier);
|
|
90
|
+
return resolveFileCandidate(resolved, JS_RESOLVE_EXTENSIONS);
|
|
91
|
+
}
|
|
92
|
+
async function resolvePythonImport(fromFile, specifier, backendRoot, absoluteRoots) {
|
|
93
|
+
if (specifier.startsWith(".")) {
|
|
94
|
+
const match = specifier.match(/^(\.+)(.*)$/);
|
|
95
|
+
if (!match) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const dotCount = match[1].length;
|
|
99
|
+
const remainder = match[2] ? match[2].replace(/\./g, "/") : "";
|
|
100
|
+
let baseDir = path.dirname(fromFile);
|
|
101
|
+
for (let i = 1; i < dotCount; i += 1) {
|
|
102
|
+
baseDir = path.dirname(baseDir);
|
|
103
|
+
}
|
|
104
|
+
const targetBase = remainder ? path.join(baseDir, remainder) : baseDir;
|
|
105
|
+
const fileCandidate = await resolveFileCandidate(targetBase, [".py"]);
|
|
106
|
+
if (fileCandidate) {
|
|
107
|
+
return fileCandidate;
|
|
108
|
+
}
|
|
109
|
+
const initCandidate = path.join(targetBase, "__init__.py");
|
|
110
|
+
if (await fileExists(initCandidate)) {
|
|
111
|
+
return initCandidate;
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const segments = specifier.split(".").filter(Boolean);
|
|
116
|
+
if (segments.length === 0) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
if (!absoluteRoots.has(segments[0])) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const targetBase = path.join(backendRoot, ...segments);
|
|
123
|
+
const fileCandidate = await resolveFileCandidate(targetBase, [".py"]);
|
|
124
|
+
if (fileCandidate) {
|
|
125
|
+
return fileCandidate;
|
|
126
|
+
}
|
|
127
|
+
const initCandidate = path.join(targetBase, "__init__.py");
|
|
128
|
+
if (await fileExists(initCandidate)) {
|
|
129
|
+
return initCandidate;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function extractPythonImportUsages(content) {
|
|
134
|
+
const usages = [];
|
|
135
|
+
for (const match of content.matchAll(/^\s*from\s+([.\w]+)\s+import\s+(.+)$/gm)) {
|
|
136
|
+
const specifier = match[1];
|
|
137
|
+
let namesPart = match[2].split("#")[0]?.trim() ?? "";
|
|
138
|
+
namesPart = namesPart.replace(/[()]/g, "");
|
|
139
|
+
const names = namesPart
|
|
140
|
+
.split(",")
|
|
141
|
+
.map((name) => name.trim())
|
|
142
|
+
.filter(Boolean);
|
|
143
|
+
if (names.includes("*")) {
|
|
144
|
+
usages.push({ specifier, symbols: [], wildcard: true });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const symbols = names
|
|
148
|
+
.map((name) => name.split(/\s+as\s+/i)[0]?.trim() ?? "")
|
|
149
|
+
.filter(Boolean);
|
|
150
|
+
usages.push({ specifier, symbols, wildcard: false });
|
|
151
|
+
}
|
|
152
|
+
for (const match of content.matchAll(/^\s*import\s+([^#\n]+)/gm)) {
|
|
153
|
+
const namesPart = match[1].split("#")[0]?.trim() ?? "";
|
|
154
|
+
const parts = namesPart
|
|
155
|
+
.split(",")
|
|
156
|
+
.map((name) => name.trim())
|
|
157
|
+
.filter(Boolean);
|
|
158
|
+
for (const part of parts) {
|
|
159
|
+
const specifier = part.split(/\s+as\s+/i)[0]?.trim() ?? "";
|
|
160
|
+
usages.push({ specifier, symbols: [], wildcard: true });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return usages;
|
|
164
|
+
}
|
|
165
|
+
function extractPythonExports(content) {
|
|
166
|
+
const exports = new Set();
|
|
167
|
+
const allMatch = content.match(/__all__\s*=\s*\[([\s\S]*?)\]/m);
|
|
168
|
+
if (allMatch) {
|
|
169
|
+
const entries = allMatch[1].match(/["']([^"']+)["']/g) ?? [];
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
exports.add(entry.replace(/["']/g, ""));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return Array.from(exports).sort((a, b) => a.localeCompare(b));
|
|
175
|
+
}
|
|
176
|
+
function normalizeEndpointPath(raw) {
|
|
177
|
+
if (!raw) {
|
|
178
|
+
return "/";
|
|
179
|
+
}
|
|
180
|
+
if (raw.startsWith("/")) {
|
|
181
|
+
return raw;
|
|
182
|
+
}
|
|
183
|
+
return `/${raw}`;
|
|
184
|
+
}
|
|
185
|
+
function extractPythonEndpoints(content) {
|
|
186
|
+
const endpoints = [];
|
|
187
|
+
const lines = content.split(/\r?\n/);
|
|
188
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
189
|
+
const line = lines[i];
|
|
190
|
+
const decoratorMatch = line.match(/^\s*@[\w.]+?\.(get|post|put|patch|delete|options|head)\s*\(\s*["']([^"']+)["']/);
|
|
191
|
+
if (decoratorMatch) {
|
|
192
|
+
const method = decoratorMatch[1].toUpperCase();
|
|
193
|
+
const pathValue = normalizeEndpointPath(decoratorMatch[2]);
|
|
194
|
+
let handler = "anonymous";
|
|
195
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
196
|
+
const next = lines[j];
|
|
197
|
+
if (next.trim().startsWith("@")) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const defMatch = next.match(/^\s*(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)/);
|
|
201
|
+
if (defMatch) {
|
|
202
|
+
handler = defMatch[1];
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
endpoints.push({ method, path: pathValue, handler, service_calls: [], ai_operations: [] });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const djangoPathMatch = line.match(/^\s*(?:path|re_path)\s*\(\s*["']([^"']+)["']\s*,\s*([A-Za-z0-9_.]+)/);
|
|
210
|
+
if (djangoPathMatch) {
|
|
211
|
+
const pathValue = normalizeEndpointPath(djangoPathMatch[1]);
|
|
212
|
+
const handler = djangoPathMatch[2];
|
|
213
|
+
endpoints.push({ method: "ANY", path: pathValue, handler, service_calls: [], ai_operations: [] });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return endpoints;
|
|
217
|
+
}
|
|
218
|
+
function extractJsEndpoints(content, filePath) {
|
|
219
|
+
const endpoints = [];
|
|
220
|
+
const source = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
221
|
+
const handlerMap = new Map();
|
|
222
|
+
const registerHandler = (name, node, typeNode) => {
|
|
223
|
+
handlerMap.set(name, { node, typeNode });
|
|
224
|
+
};
|
|
225
|
+
const collectHandlers = (node) => {
|
|
226
|
+
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
227
|
+
registerHandler(node.name.text, node);
|
|
228
|
+
}
|
|
229
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
230
|
+
if (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) {
|
|
231
|
+
registerHandler(node.name.text, node.initializer, node.type);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
ts.forEachChild(node, collectHandlers);
|
|
235
|
+
};
|
|
236
|
+
collectHandlers(source);
|
|
237
|
+
const extractHandlerName = (node) => {
|
|
238
|
+
if (!node) {
|
|
239
|
+
return "anonymous";
|
|
240
|
+
}
|
|
241
|
+
if (ts.isIdentifier(node)) {
|
|
242
|
+
return node.text;
|
|
243
|
+
}
|
|
244
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
245
|
+
return `${node.expression.getText(source)}.${node.name.text}`;
|
|
246
|
+
}
|
|
247
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
248
|
+
return node.name?.text ?? "anonymous";
|
|
249
|
+
}
|
|
250
|
+
return "anonymous";
|
|
251
|
+
};
|
|
252
|
+
const extractRoutePath = (node) => {
|
|
253
|
+
if (!node) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
257
|
+
return normalizeEndpointPath(node.text);
|
|
258
|
+
}
|
|
259
|
+
if (ts.isTemplateExpression(node)) {
|
|
260
|
+
return normalizeEndpointPath(node.getText(source));
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
};
|
|
264
|
+
const resolveHandlerInfo = (node) => {
|
|
265
|
+
if (!node) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
269
|
+
return { node };
|
|
270
|
+
}
|
|
271
|
+
if (ts.isIdentifier(node)) {
|
|
272
|
+
return handlerMap.get(node.text) ?? null;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
};
|
|
276
|
+
const extractSchemasFromHandler = (handlerInfo) => {
|
|
277
|
+
let requestSchema = null;
|
|
278
|
+
let responseSchema = null;
|
|
279
|
+
const parseRequestType = (typeNode) => {
|
|
280
|
+
if (!typeNode || !ts.isTypeReferenceNode(typeNode)) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const typeName = typeNode.typeName.getText(source);
|
|
284
|
+
const args = typeNode.typeArguments ?? [];
|
|
285
|
+
if (typeName.endsWith("RequestHandler") || typeName === "RequestHandler") {
|
|
286
|
+
if (args[2]) {
|
|
287
|
+
requestSchema = requestSchema ?? args[2].getText(source);
|
|
288
|
+
}
|
|
289
|
+
if (args[1]) {
|
|
290
|
+
responseSchema = responseSchema ?? args[1].getText(source);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (typeName.endsWith("Request")) {
|
|
294
|
+
if (args[2]) {
|
|
295
|
+
requestSchema = requestSchema ?? args[2].getText(source);
|
|
296
|
+
}
|
|
297
|
+
if (args[1]) {
|
|
298
|
+
responseSchema = responseSchema ?? args[1].getText(source);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (typeName.endsWith("Response") && args[0]) {
|
|
302
|
+
responseSchema = responseSchema ?? args[0].getText(source);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
if (handlerInfo) {
|
|
306
|
+
const params = handlerInfo.node.parameters;
|
|
307
|
+
if (params[0]?.type) {
|
|
308
|
+
parseRequestType(params[0].type);
|
|
309
|
+
}
|
|
310
|
+
if (params[1]?.type) {
|
|
311
|
+
parseRequestType(params[1].type);
|
|
312
|
+
}
|
|
313
|
+
if ((!requestSchema || !responseSchema) && handlerInfo.typeNode) {
|
|
314
|
+
parseRequestType(handlerInfo.typeNode);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { requestSchema, responseSchema };
|
|
318
|
+
};
|
|
319
|
+
const extractTokenBudget = (callNode) => {
|
|
320
|
+
const opts = callNode.arguments.find((arg) => ts.isObjectLiteralExpression(arg));
|
|
321
|
+
if (!opts || !ts.isObjectLiteralExpression(opts)) {
|
|
322
|
+
return { maxTokens: null, maxOutputTokens: null, tokenBudget: null };
|
|
323
|
+
}
|
|
324
|
+
return extractTokenBudgetFromObjectLiteral(opts);
|
|
325
|
+
};
|
|
326
|
+
const visit = (node) => {
|
|
327
|
+
if (ts.isCallExpression(node)) {
|
|
328
|
+
const expr = node.expression;
|
|
329
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
330
|
+
const method = expr.name.text;
|
|
331
|
+
if (HTTP_METHODS.has(method)) {
|
|
332
|
+
const pathValue = extractRoutePath(node.arguments[0]);
|
|
333
|
+
if (pathValue) {
|
|
334
|
+
const handlerArg = node.arguments[node.arguments.length - 1];
|
|
335
|
+
const handler = extractHandlerName(handlerArg);
|
|
336
|
+
// basic schema extraction from generic params
|
|
337
|
+
let requestSchema = null;
|
|
338
|
+
let responseSchema = null;
|
|
339
|
+
if (node.typeArguments && node.typeArguments.length > 0) {
|
|
340
|
+
const reqType = node.typeArguments[0];
|
|
341
|
+
if (reqType) {
|
|
342
|
+
requestSchema = reqType.getText(source);
|
|
343
|
+
}
|
|
344
|
+
if (node.typeArguments.length > 1) {
|
|
345
|
+
const resType = node.typeArguments[1];
|
|
346
|
+
responseSchema = resType?.getText(source) ?? null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!requestSchema || !responseSchema) {
|
|
350
|
+
const derived = extractSchemasFromHandler(resolveHandlerInfo(handlerArg));
|
|
351
|
+
requestSchema = requestSchema ?? derived.requestSchema;
|
|
352
|
+
responseSchema = responseSchema ?? derived.responseSchema;
|
|
353
|
+
}
|
|
354
|
+
let serviceCalls = [];
|
|
355
|
+
let aiOperations = [];
|
|
356
|
+
const handlerNode = handlerArg;
|
|
357
|
+
if (handlerNode) {
|
|
358
|
+
const visitHandler = (n) => {
|
|
359
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
360
|
+
const callName = `${n.expression.expression.getText(source)}.${n.expression.name.text}`;
|
|
361
|
+
serviceCalls.push(callName);
|
|
362
|
+
const lowerCall = callName.toLowerCase();
|
|
363
|
+
if (lowerCall.includes("openai") || lowerCall.includes("chatcompletions") || lowerCall.includes("anthropic")) {
|
|
364
|
+
// Very basic model extraction from second argument (options bag) for JS
|
|
365
|
+
let model = null;
|
|
366
|
+
const opts = n.arguments.find((arg) => ts.isObjectLiteralExpression(arg));
|
|
367
|
+
if (opts && ts.isObjectLiteralExpression(opts)) {
|
|
368
|
+
for (const prop of opts.properties) {
|
|
369
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === "model") {
|
|
370
|
+
model = getStringLiteral(prop.initializer) ?? null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const tokenBudget = extractTokenBudget(n);
|
|
375
|
+
aiOperations.push({
|
|
376
|
+
provider: lowerCall.includes("openai") ? "openai" : "anthropic",
|
|
377
|
+
operation: callName,
|
|
378
|
+
model,
|
|
379
|
+
max_tokens: tokenBudget.maxTokens,
|
|
380
|
+
max_output_tokens: tokenBudget.maxOutputTokens,
|
|
381
|
+
token_budget: tokenBudget.tokenBudget
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
|
|
386
|
+
serviceCalls.push(n.expression.text);
|
|
387
|
+
}
|
|
388
|
+
ts.forEachChild(n, visitHandler);
|
|
389
|
+
};
|
|
390
|
+
ts.forEachChild(handlerNode, visitHandler);
|
|
391
|
+
}
|
|
392
|
+
endpoints.push({
|
|
393
|
+
method: method.toUpperCase(),
|
|
394
|
+
path: pathValue,
|
|
395
|
+
handler,
|
|
396
|
+
request_schema: requestSchema,
|
|
397
|
+
response_schema: responseSchema,
|
|
398
|
+
service_calls: Array.from(new Set(serviceCalls)).sort(),
|
|
399
|
+
ai_operations: aiOperations
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (expr.name.text === "route" && node.arguments.length > 0) {
|
|
404
|
+
const pathValue = extractRoutePath(node.arguments[0]);
|
|
405
|
+
if (pathValue) {
|
|
406
|
+
const parent = node.parent;
|
|
407
|
+
if (parent &&
|
|
408
|
+
ts.isPropertyAccessExpression(parent) &&
|
|
409
|
+
HTTP_METHODS.has(parent.name.text)) {
|
|
410
|
+
const callParent = parent.parent;
|
|
411
|
+
if (callParent && ts.isCallExpression(callParent)) {
|
|
412
|
+
const handlerArg = callParent.arguments[callParent.arguments.length - 1];
|
|
413
|
+
const handler = extractHandlerName(handlerArg);
|
|
414
|
+
let requestSchema = null;
|
|
415
|
+
let responseSchema = null;
|
|
416
|
+
if (callParent.typeArguments && callParent.typeArguments.length > 0) {
|
|
417
|
+
const reqType = callParent.typeArguments[0];
|
|
418
|
+
if (reqType) {
|
|
419
|
+
requestSchema = reqType.getText(source);
|
|
420
|
+
}
|
|
421
|
+
if (callParent.typeArguments.length > 1) {
|
|
422
|
+
const resType = callParent.typeArguments[1];
|
|
423
|
+
responseSchema = resType?.getText(source) ?? null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (!requestSchema || !responseSchema) {
|
|
427
|
+
const derived = extractSchemasFromHandler(resolveHandlerInfo(handlerArg));
|
|
428
|
+
requestSchema = requestSchema ?? derived.requestSchema;
|
|
429
|
+
responseSchema = responseSchema ?? derived.responseSchema;
|
|
430
|
+
}
|
|
431
|
+
let serviceCalls = [];
|
|
432
|
+
let aiOperations = [];
|
|
433
|
+
const handlerNode = handlerArg;
|
|
434
|
+
if (handlerNode) {
|
|
435
|
+
const visitHandler = (n) => {
|
|
436
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression)) {
|
|
437
|
+
const callName = `${n.expression.expression.getText(source)}.${n.expression.name.text}`;
|
|
438
|
+
serviceCalls.push(callName);
|
|
439
|
+
const lowerCall = callName.toLowerCase();
|
|
440
|
+
if (lowerCall.includes("openai") || lowerCall.includes("chatcompletions") || lowerCall.includes("anthropic")) {
|
|
441
|
+
let model = null;
|
|
442
|
+
const opts = n.arguments.find((arg) => ts.isObjectLiteralExpression(arg));
|
|
443
|
+
if (opts && ts.isObjectLiteralExpression(opts)) {
|
|
444
|
+
for (const prop of opts.properties) {
|
|
445
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === "model") {
|
|
446
|
+
model = getStringLiteral(prop.initializer) ?? null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const tokenBudget = extractTokenBudget(n);
|
|
451
|
+
aiOperations.push({
|
|
452
|
+
provider: lowerCall.includes("openai") ? "openai" : "anthropic",
|
|
453
|
+
operation: callName,
|
|
454
|
+
model,
|
|
455
|
+
max_tokens: tokenBudget.maxTokens,
|
|
456
|
+
max_output_tokens: tokenBudget.maxOutputTokens,
|
|
457
|
+
token_budget: tokenBudget.tokenBudget
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
else if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) {
|
|
462
|
+
serviceCalls.push(n.expression.text);
|
|
463
|
+
}
|
|
464
|
+
ts.forEachChild(n, visitHandler);
|
|
465
|
+
};
|
|
466
|
+
ts.forEachChild(handlerNode, visitHandler);
|
|
467
|
+
}
|
|
468
|
+
endpoints.push({
|
|
469
|
+
method: parent.name.text.toUpperCase(),
|
|
470
|
+
path: pathValue,
|
|
471
|
+
handler,
|
|
472
|
+
request_schema: requestSchema,
|
|
473
|
+
response_schema: responseSchema,
|
|
474
|
+
service_calls: Array.from(new Set(serviceCalls)).sort(),
|
|
475
|
+
ai_operations: aiOperations
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
ts.forEachChild(node, visit);
|
|
484
|
+
};
|
|
485
|
+
visit(source);
|
|
486
|
+
return endpoints;
|
|
487
|
+
}
|
|
488
|
+
function extractCeleryTasks(content) {
|
|
489
|
+
const tasks = [];
|
|
490
|
+
const lines = content.split(/\r?\n/);
|
|
491
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
492
|
+
const line = lines[i];
|
|
493
|
+
const decoratorMatch = line.match(/^\s*@(?:\w+\.)?(shared_task|task)\b(?:\(([^)]*)\))?/);
|
|
494
|
+
if (!decoratorMatch) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
let queue = null;
|
|
498
|
+
const args = decoratorMatch[2] ?? "";
|
|
499
|
+
const queueMatch = args.match(/queue\s*=\s*["']([^"']+)["']/);
|
|
500
|
+
if (queueMatch) {
|
|
501
|
+
queue = queueMatch[1];
|
|
502
|
+
}
|
|
503
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
504
|
+
const next = lines[j];
|
|
505
|
+
if (next.trim().startsWith("@")) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const defMatch = next.match(/^\s*(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)/);
|
|
509
|
+
if (defMatch) {
|
|
510
|
+
tasks.push({ name: defMatch[1], queue });
|
|
511
|
+
}
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return tasks;
|
|
516
|
+
}
|
|
517
|
+
function extractSqlAlchemyModels(content, file) {
|
|
518
|
+
const models = [];
|
|
519
|
+
const lines = content.split(/\r?\n/);
|
|
520
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
521
|
+
const line = lines[i];
|
|
522
|
+
const classMatch = line.match(/^(\s*)class\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*:/);
|
|
523
|
+
if (!classMatch) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const indent = classMatch[1].length;
|
|
527
|
+
const name = classMatch[2];
|
|
528
|
+
const bases = classMatch[3];
|
|
529
|
+
const isSqlAlchemy = /\\bBase\\b/.test(bases) ||
|
|
530
|
+
/DeclarativeBase/.test(bases) ||
|
|
531
|
+
/db\\.Model/.test(bases) ||
|
|
532
|
+
/SQLAlchemy/.test(content);
|
|
533
|
+
if (!isSqlAlchemy) {
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const fields = new Set();
|
|
537
|
+
const relationships = new Set();
|
|
538
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
539
|
+
const bodyLine = lines[j];
|
|
540
|
+
const bodyIndent = bodyLine.match(/^\s*/)?.[0].length ?? 0;
|
|
541
|
+
if (bodyIndent <= indent && bodyLine.trim()) {
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
const fieldMatch = bodyLine.match(/^\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(Column|mapped_column)\s*\(/);
|
|
545
|
+
if (fieldMatch) {
|
|
546
|
+
fields.add(fieldMatch[1]);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const relMatch = bodyLine.match(/^\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*relationship\s*\(/);
|
|
550
|
+
if (relMatch) {
|
|
551
|
+
relationships.add(relMatch[1]);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
models.push({
|
|
555
|
+
name,
|
|
556
|
+
file,
|
|
557
|
+
framework: "sqlalchemy",
|
|
558
|
+
fields: Array.from(fields).sort((a, b) => a.localeCompare(b)),
|
|
559
|
+
relationships: Array.from(relationships).sort((a, b) => a.localeCompare(b))
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
return models;
|
|
563
|
+
}
|
|
564
|
+
function extractDjangoModels(content, file) {
|
|
565
|
+
const models = [];
|
|
566
|
+
const lines = content.split(/\r?\n/);
|
|
567
|
+
const hasDjangoImport = /django\\.db/.test(content);
|
|
568
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
569
|
+
const line = lines[i];
|
|
570
|
+
const classMatch = line.match(/^(\s*)class\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*:/);
|
|
571
|
+
if (!classMatch) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const indent = classMatch[1].length;
|
|
575
|
+
const name = classMatch[2];
|
|
576
|
+
const bases = classMatch[3];
|
|
577
|
+
const isDjango = /models\\.Model/.test(bases) || (hasDjangoImport && /\\bModel\\b/.test(bases));
|
|
578
|
+
if (!isDjango) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
const fields = new Set();
|
|
582
|
+
const relationships = new Set();
|
|
583
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
584
|
+
const bodyLine = lines[j];
|
|
585
|
+
const bodyIndent = bodyLine.match(/^\s*/)?.[0].length ?? 0;
|
|
586
|
+
if (bodyIndent <= indent && bodyLine.trim()) {
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
const fieldMatch = bodyLine.match(/^\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*models\.([A-Za-z0-9_]+)\s*\(/);
|
|
590
|
+
if (fieldMatch) {
|
|
591
|
+
const fieldName = fieldMatch[1];
|
|
592
|
+
const fieldType = fieldMatch[2];
|
|
593
|
+
fields.add(fieldName);
|
|
594
|
+
if (/ForeignKey|ManyToMany|OneToOne/.test(fieldType)) {
|
|
595
|
+
relationships.add(`${fieldName}:${fieldType}`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
models.push({
|
|
600
|
+
name,
|
|
601
|
+
file,
|
|
602
|
+
framework: "django",
|
|
603
|
+
fields: Array.from(fields).sort((a, b) => a.localeCompare(b)),
|
|
604
|
+
relationships: Array.from(relationships).sort((a, b) => a.localeCompare(b))
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return models;
|
|
608
|
+
}
|
|
609
|
+
function scriptKindFromPath(filePath) {
|
|
610
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
611
|
+
switch (ext) {
|
|
612
|
+
case ".tsx":
|
|
613
|
+
return ts.ScriptKind.TSX;
|
|
614
|
+
case ".jsx":
|
|
615
|
+
return ts.ScriptKind.JSX;
|
|
616
|
+
case ".js":
|
|
617
|
+
case ".mjs":
|
|
618
|
+
case ".cjs":
|
|
619
|
+
return ts.ScriptKind.JS;
|
|
620
|
+
case ".ts":
|
|
621
|
+
default:
|
|
622
|
+
return ts.ScriptKind.TS;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function getStringLiteral(node) {
|
|
626
|
+
if (!node) {
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
630
|
+
return node.text;
|
|
631
|
+
}
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
function getNumberLiteral(node) {
|
|
635
|
+
if (!node) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
if (ts.isNumericLiteral(node)) {
|
|
639
|
+
const parsed = Number(node.text);
|
|
640
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
641
|
+
}
|
|
642
|
+
if (ts.isPrefixUnaryExpression(node) && ts.isNumericLiteral(node.operand)) {
|
|
643
|
+
const sign = node.operator === ts.SyntaxKind.MinusToken ? -1 : 1;
|
|
644
|
+
const parsed = Number(node.operand.text);
|
|
645
|
+
return Number.isFinite(parsed) ? sign * parsed : null;
|
|
646
|
+
}
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
function getPropertyNameText(name) {
|
|
650
|
+
if (ts.isIdentifier(name)) {
|
|
651
|
+
return name.text;
|
|
652
|
+
}
|
|
653
|
+
if (ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) {
|
|
654
|
+
return name.text;
|
|
655
|
+
}
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
function extractTokenBudgetFromObjectLiteral(obj) {
|
|
659
|
+
let maxTokens = null;
|
|
660
|
+
let maxOutputTokens = null;
|
|
661
|
+
for (const prop of obj.properties) {
|
|
662
|
+
if (!ts.isPropertyAssignment(prop)) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
const key = getPropertyNameText(prop.name);
|
|
666
|
+
if (!key) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
const value = getNumberLiteral(prop.initializer);
|
|
670
|
+
if (value === null) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
switch (key) {
|
|
674
|
+
case "max_tokens":
|
|
675
|
+
case "maxTokens":
|
|
676
|
+
case "max_completion_tokens":
|
|
677
|
+
case "maxCompletionTokens":
|
|
678
|
+
case "max_new_tokens":
|
|
679
|
+
case "maxNewTokens":
|
|
680
|
+
case "token_limit":
|
|
681
|
+
case "tokenLimit":
|
|
682
|
+
case "tokens":
|
|
683
|
+
maxTokens = value;
|
|
684
|
+
break;
|
|
685
|
+
case "max_output_tokens":
|
|
686
|
+
case "maxOutputTokens":
|
|
687
|
+
maxOutputTokens = value;
|
|
688
|
+
break;
|
|
689
|
+
default:
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const tokenBudget = maxOutputTokens ?? maxTokens;
|
|
694
|
+
return { maxTokens, maxOutputTokens, tokenBudget };
|
|
695
|
+
}
|
|
696
|
+
function parseJsFile(content, filePath) {
|
|
697
|
+
const imports = new Set();
|
|
698
|
+
const usages = [];
|
|
699
|
+
const exports = new Set();
|
|
700
|
+
const exportDetailMap = new Map();
|
|
701
|
+
const source = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindFromPath(filePath));
|
|
702
|
+
const addUsage = (specifier, symbols, wildcard) => {
|
|
703
|
+
imports.add(specifier);
|
|
704
|
+
usages.push({ specifier, symbols, wildcard });
|
|
705
|
+
};
|
|
706
|
+
const addExport = (name) => {
|
|
707
|
+
if (name) {
|
|
708
|
+
exports.add(name);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
const addExportDetail = (detail) => {
|
|
712
|
+
const key = `${detail.kind}|${detail.name}|${detail.alias ?? ""}`;
|
|
713
|
+
exportDetailMap.set(key, detail);
|
|
714
|
+
};
|
|
715
|
+
const handleExportedDeclaration = (node, name) => {
|
|
716
|
+
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
|
|
717
|
+
const isDefault = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
|
718
|
+
if (isDefault) {
|
|
719
|
+
exports.add("default");
|
|
720
|
+
addExportDetail({
|
|
721
|
+
name: name?.text ?? "default",
|
|
722
|
+
kind: "default"
|
|
723
|
+
});
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
if (name?.text) {
|
|
727
|
+
exports.add(name.text);
|
|
728
|
+
addExportDetail({
|
|
729
|
+
name: name.text,
|
|
730
|
+
kind: "named"
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
const visit = (node) => {
|
|
735
|
+
if (ts.isImportDeclaration(node)) {
|
|
736
|
+
const specifier = getStringLiteral(node.moduleSpecifier);
|
|
737
|
+
if (specifier) {
|
|
738
|
+
const symbols = [];
|
|
739
|
+
let wildcard = false;
|
|
740
|
+
const clause = node.importClause;
|
|
741
|
+
if (clause) {
|
|
742
|
+
if (clause.name) {
|
|
743
|
+
symbols.push("default");
|
|
744
|
+
}
|
|
745
|
+
if (clause.namedBindings) {
|
|
746
|
+
if (ts.isNamespaceImport(clause.namedBindings)) {
|
|
747
|
+
wildcard = true;
|
|
748
|
+
}
|
|
749
|
+
else if (ts.isNamedImports(clause.namedBindings)) {
|
|
750
|
+
for (const element of clause.namedBindings.elements) {
|
|
751
|
+
const original = element.propertyName?.text ?? element.name.text;
|
|
752
|
+
symbols.push(original);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
addUsage(specifier, symbols, wildcard);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
else if (ts.isImportEqualsDeclaration(node)) {
|
|
761
|
+
if (ts.isExternalModuleReference(node.moduleReference)) {
|
|
762
|
+
const specifier = getStringLiteral(node.moduleReference.expression);
|
|
763
|
+
if (specifier) {
|
|
764
|
+
addUsage(specifier, [], true);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
else if (ts.isExportDeclaration(node)) {
|
|
769
|
+
const specifier = node.moduleSpecifier ? getStringLiteral(node.moduleSpecifier) : null;
|
|
770
|
+
if (specifier) {
|
|
771
|
+
if (!node.exportClause) {
|
|
772
|
+
addUsage(specifier, [], true);
|
|
773
|
+
}
|
|
774
|
+
else if (ts.isNamedExports(node.exportClause)) {
|
|
775
|
+
const symbols = node.exportClause.elements.map((element) => element.propertyName?.text ?? element.name.text);
|
|
776
|
+
addUsage(specifier, symbols, false);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
else if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
780
|
+
for (const element of node.exportClause.elements) {
|
|
781
|
+
exports.add(element.name.text);
|
|
782
|
+
addExportDetail({
|
|
783
|
+
name: element.propertyName?.text ?? element.name.text,
|
|
784
|
+
kind: "named",
|
|
785
|
+
alias: element.propertyName && element.propertyName.text !== element.name.text
|
|
786
|
+
? element.name.text
|
|
787
|
+
: undefined
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
else if (ts.isExportAssignment(node)) {
|
|
793
|
+
exports.add("default");
|
|
794
|
+
addExportDetail({
|
|
795
|
+
name: ts.isIdentifier(node.expression) ? node.expression.text : "default",
|
|
796
|
+
kind: "default"
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
else if (ts.isFunctionDeclaration(node)) {
|
|
800
|
+
if (node.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
801
|
+
handleExportedDeclaration(node, node.name);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
else if (ts.isClassDeclaration(node)) {
|
|
805
|
+
if (node.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
806
|
+
handleExportedDeclaration(node, node.name);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
else if (ts.isInterfaceDeclaration(node)) {
|
|
810
|
+
if (node.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
811
|
+
handleExportedDeclaration(node, node.name);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
else if (ts.isTypeAliasDeclaration(node)) {
|
|
815
|
+
if (node.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
816
|
+
handleExportedDeclaration(node, node.name);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
else if (ts.isEnumDeclaration(node)) {
|
|
820
|
+
if (node.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
821
|
+
handleExportedDeclaration(node, node.name);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
else if (ts.isVariableStatement(node)) {
|
|
825
|
+
if (node.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
826
|
+
for (const declaration of node.declarationList.declarations) {
|
|
827
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
828
|
+
exports.add(declaration.name.text);
|
|
829
|
+
addExportDetail({
|
|
830
|
+
name: declaration.name.text,
|
|
831
|
+
kind: "named"
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
else if (ts.isCallExpression(node)) {
|
|
838
|
+
if (node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
839
|
+
const specifier = getStringLiteral(node.arguments[0]);
|
|
840
|
+
if (specifier) {
|
|
841
|
+
addUsage(specifier, [], true);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
else if (ts.isIdentifier(node.expression) && node.expression.text === "require") {
|
|
845
|
+
const specifier = getStringLiteral(node.arguments[0]);
|
|
846
|
+
if (specifier) {
|
|
847
|
+
addUsage(specifier, [], true);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
ts.forEachChild(node, visit);
|
|
852
|
+
};
|
|
853
|
+
visit(source);
|
|
854
|
+
return {
|
|
855
|
+
imports: Array.from(imports).sort((a, b) => a.localeCompare(b)),
|
|
856
|
+
usages,
|
|
857
|
+
exports: Array.from(exports).sort((a, b) => a.localeCompare(b)),
|
|
858
|
+
exportDetails: Array.from(exportDetailMap.values()).sort((a, b) => {
|
|
859
|
+
const kind = a.kind.localeCompare(b.kind);
|
|
860
|
+
if (kind !== 0) {
|
|
861
|
+
return kind;
|
|
862
|
+
}
|
|
863
|
+
const name = a.name.localeCompare(b.name);
|
|
864
|
+
if (name !== 0) {
|
|
865
|
+
return name;
|
|
866
|
+
}
|
|
867
|
+
return (a.alias ?? "").localeCompare(b.alias ?? "");
|
|
868
|
+
})
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function classifyLayer(inbound, outbound) {
|
|
872
|
+
if (inbound === 0 && outbound === 0) {
|
|
873
|
+
return "isolated";
|
|
874
|
+
}
|
|
875
|
+
if (outbound === 0 && inbound > 0) {
|
|
876
|
+
return "core";
|
|
877
|
+
}
|
|
878
|
+
if (inbound === 0 && outbound > 0) {
|
|
879
|
+
return "top";
|
|
880
|
+
}
|
|
881
|
+
return "middle";
|
|
882
|
+
}
|
|
883
|
+
async function resolveEntrypointCandidates(entry, baseDir) {
|
|
884
|
+
const candidates = [];
|
|
885
|
+
const absPath = path.resolve(baseDir, entry);
|
|
886
|
+
candidates.push(absPath);
|
|
887
|
+
const ext = path.extname(absPath);
|
|
888
|
+
if (ext) {
|
|
889
|
+
const withoutExt = absPath.slice(0, -ext.length);
|
|
890
|
+
candidates.push(`${withoutExt}.ts`);
|
|
891
|
+
candidates.push(`${withoutExt}.tsx`);
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
candidates.push(`${absPath}.ts`);
|
|
895
|
+
candidates.push(`${absPath}.tsx`);
|
|
896
|
+
candidates.push(`${absPath}.js`);
|
|
897
|
+
candidates.push(`${absPath}.jsx`);
|
|
898
|
+
candidates.push(`${absPath}.mjs`);
|
|
899
|
+
candidates.push(`${absPath}.cjs`);
|
|
900
|
+
}
|
|
901
|
+
if (absPath.includes(`${path.sep}dist${path.sep}`)) {
|
|
902
|
+
candidates.push(absPath.replace(`${path.sep}dist${path.sep}`, `${path.sep}src${path.sep}`));
|
|
903
|
+
}
|
|
904
|
+
if (absPath.includes(`${path.sep}lib${path.sep}`)) {
|
|
905
|
+
candidates.push(absPath.replace(`${path.sep}lib${path.sep}`, `${path.sep}src${path.sep}`));
|
|
906
|
+
}
|
|
907
|
+
return candidates;
|
|
908
|
+
}
|
|
909
|
+
async function detectEntrypoints(backendRoot, baseRoot) {
|
|
910
|
+
const entrypoints = new Set();
|
|
911
|
+
const packageCandidates = [
|
|
912
|
+
path.join(backendRoot, "package.json"),
|
|
913
|
+
path.join(baseRoot, "package.json")
|
|
914
|
+
];
|
|
915
|
+
for (const pkgPath of packageCandidates) {
|
|
916
|
+
let pkgRaw = null;
|
|
917
|
+
try {
|
|
918
|
+
pkgRaw = await fs.readFile(pkgPath, "utf8");
|
|
919
|
+
}
|
|
920
|
+
catch {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
let pkg = null;
|
|
924
|
+
try {
|
|
925
|
+
pkg = JSON.parse(pkgRaw);
|
|
926
|
+
}
|
|
927
|
+
catch {
|
|
928
|
+
pkg = null;
|
|
929
|
+
}
|
|
930
|
+
if (!pkg) {
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
const baseDir = path.dirname(pkgPath);
|
|
934
|
+
const entries = [];
|
|
935
|
+
if (pkg.main) {
|
|
936
|
+
entries.push(pkg.main);
|
|
937
|
+
}
|
|
938
|
+
if (pkg.bin) {
|
|
939
|
+
if (typeof pkg.bin === "string") {
|
|
940
|
+
entries.push(pkg.bin);
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
entries.push(...Object.values(pkg.bin));
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
for (const entry of entries) {
|
|
947
|
+
const candidates = await resolveEntrypointCandidates(entry, baseDir);
|
|
948
|
+
for (const candidate of candidates) {
|
|
949
|
+
const resolved = await resolveFileCandidate(candidate, JS_RESOLVE_EXTENSIONS);
|
|
950
|
+
if (resolved) {
|
|
951
|
+
entrypoints.add(toPosix(path.relative(baseRoot, resolved)));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
const pyMain = path.join(backendRoot, "__main__.py");
|
|
957
|
+
if (await fileExists(pyMain)) {
|
|
958
|
+
entrypoints.add(toPosix(path.relative(baseRoot, pyMain)));
|
|
959
|
+
}
|
|
960
|
+
return entrypoints;
|
|
961
|
+
}
|
|
962
|
+
async function detectPythonPackageRoots(backendRoot, ignore) {
|
|
963
|
+
const roots = [];
|
|
964
|
+
let entries = [];
|
|
965
|
+
try {
|
|
966
|
+
entries = await fs.readdir(backendRoot, { withFileTypes: true });
|
|
967
|
+
}
|
|
968
|
+
catch {
|
|
969
|
+
return roots;
|
|
970
|
+
}
|
|
971
|
+
for (const entry of entries) {
|
|
972
|
+
if (!entry.isDirectory()) {
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
const fullPath = path.join(backendRoot, entry.name);
|
|
976
|
+
if (ignore.isIgnoredDir(entry.name, fullPath)) {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
const initPath = path.join(fullPath, "__init__.py");
|
|
980
|
+
if (await fileExists(initPath)) {
|
|
981
|
+
roots.push(entry.name);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return roots.sort((a, b) => a.localeCompare(b));
|
|
985
|
+
}
|
|
986
|
+
function extractJsEnums(content, filePath) {
|
|
987
|
+
const enums = [];
|
|
988
|
+
const source = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindFromPath(filePath));
|
|
989
|
+
const visit = (node) => {
|
|
990
|
+
if (ts.isEnumDeclaration(node)) {
|
|
991
|
+
const name = node.name.text;
|
|
992
|
+
const values = [];
|
|
993
|
+
for (const member of node.members) {
|
|
994
|
+
if (ts.isIdentifier(member.name)) {
|
|
995
|
+
values.push(member.name.text);
|
|
996
|
+
}
|
|
997
|
+
else if (ts.isStringLiteral(member.name)) {
|
|
998
|
+
values.push(member.name.text);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
enums.push({ name, file: filePath, values });
|
|
1002
|
+
}
|
|
1003
|
+
ts.forEachChild(node, visit);
|
|
1004
|
+
};
|
|
1005
|
+
visit(source);
|
|
1006
|
+
return enums;
|
|
1007
|
+
}
|
|
1008
|
+
function extractJsConstants(content, filePath) {
|
|
1009
|
+
const constants = [];
|
|
1010
|
+
const source = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKindFromPath(filePath));
|
|
1011
|
+
const visit = (node) => {
|
|
1012
|
+
if (ts.isVariableStatement(node)) {
|
|
1013
|
+
if (node.declarationList.flags & ts.NodeFlags.Const) {
|
|
1014
|
+
for (const declaration of node.declarationList.declarations) {
|
|
1015
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
1016
|
+
const name = declaration.name.text;
|
|
1017
|
+
if (name === name.toUpperCase() && name !== "_") {
|
|
1018
|
+
const type = declaration.type ? declaration.type.getText(source) : "unknown";
|
|
1019
|
+
const value = declaration.initializer ? declaration.initializer.getText(source) : "unknown";
|
|
1020
|
+
constants.push({ name, file: filePath, type, value });
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
ts.forEachChild(node, visit);
|
|
1027
|
+
};
|
|
1028
|
+
visit(source);
|
|
1029
|
+
return constants;
|
|
1030
|
+
}
|
|
1031
|
+
function emptyPythonFileResult() {
|
|
1032
|
+
return {
|
|
1033
|
+
importUsages: [],
|
|
1034
|
+
exports: [],
|
|
1035
|
+
exportDetails: [],
|
|
1036
|
+
endpoints: [],
|
|
1037
|
+
dataModels: [],
|
|
1038
|
+
tasks: [],
|
|
1039
|
+
enums: [],
|
|
1040
|
+
constants: [],
|
|
1041
|
+
endpointModelUsage: []
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
export async function analyzeBackend(backendRoot, config) {
|
|
1045
|
+
const root = path.resolve(backendRoot);
|
|
1046
|
+
const baseRoot = path.dirname(root);
|
|
1047
|
+
const ignore = createIgnoreMatcher(config, baseRoot);
|
|
1048
|
+
const pythonPackageRoots = await detectPythonPackageRoots(root, ignore);
|
|
1049
|
+
const absoluteImportRoots = new Set([
|
|
1050
|
+
...pythonPackageRoots,
|
|
1051
|
+
...(config.python?.absoluteImportRoots ?? [])
|
|
1052
|
+
]);
|
|
1053
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
1054
|
+
const modules = [];
|
|
1055
|
+
const rootFiles = [];
|
|
1056
|
+
for (const entry of entries) {
|
|
1057
|
+
if (!entry.isDirectory()) {
|
|
1058
|
+
if (entry.isFile()) {
|
|
1059
|
+
const absoluteFile = path.join(root, entry.name);
|
|
1060
|
+
const relative = path.relative(baseRoot, absoluteFile);
|
|
1061
|
+
if (isCodeFile(absoluteFile) && !ignore.isIgnoredPath(relative)) {
|
|
1062
|
+
rootFiles.push(toPosix(relative));
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
if (ignore.isIgnoredDir(entry.name, path.join(root, entry.name))) {
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
const moduleRoot = path.join(root, entry.name);
|
|
1071
|
+
const files = (await listFiles(moduleRoot, ignore, baseRoot))
|
|
1072
|
+
.map((file) => toPosix(path.relative(baseRoot, file)))
|
|
1073
|
+
.sort((a, b) => a.localeCompare(b));
|
|
1074
|
+
modules.push({
|
|
1075
|
+
id: entry.name,
|
|
1076
|
+
path: toPosix(path.relative(baseRoot, moduleRoot)),
|
|
1077
|
+
type: "backend",
|
|
1078
|
+
layer: "isolated",
|
|
1079
|
+
files,
|
|
1080
|
+
endpoints: [],
|
|
1081
|
+
imports: [],
|
|
1082
|
+
exports: []
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
modules.sort((a, b) => a.id.localeCompare(b.id));
|
|
1086
|
+
const fileToModule = new Map();
|
|
1087
|
+
const moduleImports = new Map();
|
|
1088
|
+
const codeFiles = [];
|
|
1089
|
+
const fileExports = new Map();
|
|
1090
|
+
const fileContents = new Map();
|
|
1091
|
+
const endpoints = [];
|
|
1092
|
+
const endpointKeys = new Set();
|
|
1093
|
+
const dataModels = [];
|
|
1094
|
+
const dataModelKeys = new Set();
|
|
1095
|
+
const tasks = [];
|
|
1096
|
+
const enums = [];
|
|
1097
|
+
const constants = [];
|
|
1098
|
+
for (const module of modules) {
|
|
1099
|
+
moduleImports.set(module.id, new Set());
|
|
1100
|
+
for (const file of module.files) {
|
|
1101
|
+
fileToModule.set(file, module.id);
|
|
1102
|
+
if (isCodeFile(file)) {
|
|
1103
|
+
codeFiles.push(file);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
for (const file of rootFiles) {
|
|
1108
|
+
if (!codeFiles.includes(file)) {
|
|
1109
|
+
codeFiles.push(file);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
codeFiles.sort((a, b) => a.localeCompare(b));
|
|
1113
|
+
const knownFiles = new Set(codeFiles);
|
|
1114
|
+
const { cachePath, cache } = await loadBackendExtractionCache({
|
|
1115
|
+
projectRoot: baseRoot,
|
|
1116
|
+
config
|
|
1117
|
+
});
|
|
1118
|
+
const activeAbsoluteFiles = new Set(codeFiles.map((file) => path.join(baseRoot, file)));
|
|
1119
|
+
for (const cachedFile of Object.keys(cache.files)) {
|
|
1120
|
+
if (!activeAbsoluteFiles.has(cachedFile)) {
|
|
1121
|
+
delete cache.files[cachedFile];
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
const pythonFilesAbs = codeFiles
|
|
1125
|
+
.filter((file) => path.extname(file) === ".py")
|
|
1126
|
+
.map((file) => path.join(baseRoot, file));
|
|
1127
|
+
const pythonResultsByFile = new Map();
|
|
1128
|
+
let canReusePythonCache = pythonFilesAbs.length > 0;
|
|
1129
|
+
for (const absoluteFile of pythonFilesAbs) {
|
|
1130
|
+
const cached = cache.files[absoluteFile];
|
|
1131
|
+
if (!cached || cached.language !== "python") {
|
|
1132
|
+
canReusePythonCache = false;
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
try {
|
|
1136
|
+
const stat = await fs.stat(absoluteFile);
|
|
1137
|
+
if (cached.mtime !== stat.mtimeMs) {
|
|
1138
|
+
const content = await fs.readFile(absoluteFile, "utf8");
|
|
1139
|
+
const hash = hashContent(content);
|
|
1140
|
+
if (hash !== cached.hash) {
|
|
1141
|
+
canReusePythonCache = false;
|
|
1142
|
+
break;
|
|
1143
|
+
}
|
|
1144
|
+
cached.mtime = stat.mtimeMs;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
canReusePythonCache = false;
|
|
1149
|
+
break;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (canReusePythonCache) {
|
|
1153
|
+
for (const absoluteFile of pythonFilesAbs) {
|
|
1154
|
+
const cached = cache.files[absoluteFile];
|
|
1155
|
+
if (!cached) {
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
pythonResultsByFile.set(toPosix(path.relative(baseRoot, absoluteFile)), {
|
|
1159
|
+
importUsages: cached.importUsages,
|
|
1160
|
+
exports: cached.exports,
|
|
1161
|
+
exportDetails: cached.exportDetails,
|
|
1162
|
+
endpoints: cached.endpoints,
|
|
1163
|
+
dataModels: cached.dataModels,
|
|
1164
|
+
tasks: cached.tasks,
|
|
1165
|
+
enums: cached.enums,
|
|
1166
|
+
constants: cached.constants,
|
|
1167
|
+
endpointModelUsage: cached.endpointModelUsage
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
else if (pythonFilesAbs.length > 0) {
|
|
1172
|
+
const pythonAst = extractPythonAst(pythonFilesAbs);
|
|
1173
|
+
const grouped = new Map();
|
|
1174
|
+
for (const absoluteFile of pythonFilesAbs) {
|
|
1175
|
+
grouped.set(toPosix(path.relative(baseRoot, absoluteFile)), emptyPythonFileResult());
|
|
1176
|
+
}
|
|
1177
|
+
if (pythonAst) {
|
|
1178
|
+
for (const endpoint of pythonAst.endpoints) {
|
|
1179
|
+
const relativeFile = toPosix(path.relative(baseRoot, endpoint.file));
|
|
1180
|
+
const entry = grouped.get(relativeFile) ?? emptyPythonFileResult();
|
|
1181
|
+
entry.endpoints.push({
|
|
1182
|
+
id: "",
|
|
1183
|
+
method: endpoint.method,
|
|
1184
|
+
path: endpoint.path,
|
|
1185
|
+
handler: endpoint.handler,
|
|
1186
|
+
file: relativeFile,
|
|
1187
|
+
module: fileToModule.get(relativeFile) ?? "root",
|
|
1188
|
+
request_schema: endpoint.request_schema,
|
|
1189
|
+
response_schema: endpoint.response_schema,
|
|
1190
|
+
service_calls: endpoint.service_calls,
|
|
1191
|
+
ai_operations: endpoint.ai_operations
|
|
1192
|
+
});
|
|
1193
|
+
grouped.set(relativeFile, entry);
|
|
1194
|
+
}
|
|
1195
|
+
for (const model of pythonAst.models) {
|
|
1196
|
+
const relativeFile = toPosix(path.relative(baseRoot, model.file));
|
|
1197
|
+
const entry = grouped.get(relativeFile) ?? emptyPythonFileResult();
|
|
1198
|
+
entry.dataModels.push({
|
|
1199
|
+
...model,
|
|
1200
|
+
file: relativeFile
|
|
1201
|
+
});
|
|
1202
|
+
grouped.set(relativeFile, entry);
|
|
1203
|
+
}
|
|
1204
|
+
for (const task of pythonAst.tasks) {
|
|
1205
|
+
const relativeFile = toPosix(path.relative(baseRoot, task.file));
|
|
1206
|
+
const entry = grouped.get(relativeFile) ?? emptyPythonFileResult();
|
|
1207
|
+
entry.tasks.push({
|
|
1208
|
+
name: task.name,
|
|
1209
|
+
file: relativeFile,
|
|
1210
|
+
kind: task.kind,
|
|
1211
|
+
queue: task.queue ?? null,
|
|
1212
|
+
schedule: task.schedule ?? null
|
|
1213
|
+
});
|
|
1214
|
+
grouped.set(relativeFile, entry);
|
|
1215
|
+
}
|
|
1216
|
+
for (const item of pythonAst.enums) {
|
|
1217
|
+
const relativeFile = toPosix(path.relative(baseRoot, item.file));
|
|
1218
|
+
const entry = grouped.get(relativeFile) ?? emptyPythonFileResult();
|
|
1219
|
+
entry.enums.push({ ...item, file: relativeFile });
|
|
1220
|
+
grouped.set(relativeFile, entry);
|
|
1221
|
+
}
|
|
1222
|
+
for (const item of pythonAst.constants) {
|
|
1223
|
+
const relativeFile = toPosix(path.relative(baseRoot, item.file));
|
|
1224
|
+
const entry = grouped.get(relativeFile) ?? emptyPythonFileResult();
|
|
1225
|
+
entry.constants.push({ ...item, file: relativeFile });
|
|
1226
|
+
grouped.set(relativeFile, entry);
|
|
1227
|
+
}
|
|
1228
|
+
for (const usage of pythonAst.endpoint_model_usage) {
|
|
1229
|
+
const relativeFile = toPosix(path.relative(baseRoot, usage.file));
|
|
1230
|
+
const entry = grouped.get(relativeFile) ?? emptyPythonFileResult();
|
|
1231
|
+
entry.endpointModelUsage.push({
|
|
1232
|
+
handler: usage.handler,
|
|
1233
|
+
models: usage.models
|
|
1234
|
+
});
|
|
1235
|
+
grouped.set(relativeFile, entry);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
for (const absoluteFile of pythonFilesAbs) {
|
|
1239
|
+
const relativeFile = toPosix(path.relative(baseRoot, absoluteFile));
|
|
1240
|
+
const content = await fs.readFile(absoluteFile, "utf8");
|
|
1241
|
+
const stat = await fs.stat(absoluteFile);
|
|
1242
|
+
const hash = hashContent(content);
|
|
1243
|
+
const entry = grouped.get(relativeFile) ?? emptyPythonFileResult();
|
|
1244
|
+
entry.importUsages = extractPythonImportUsages(content);
|
|
1245
|
+
entry.exports = extractPythonExports(content);
|
|
1246
|
+
pythonResultsByFile.set(relativeFile, entry);
|
|
1247
|
+
cache.files[absoluteFile] = {
|
|
1248
|
+
hash,
|
|
1249
|
+
mtime: stat.mtimeMs,
|
|
1250
|
+
language: "python",
|
|
1251
|
+
importUsages: entry.importUsages,
|
|
1252
|
+
exports: entry.exports,
|
|
1253
|
+
exportDetails: [],
|
|
1254
|
+
endpoints: entry.endpoints,
|
|
1255
|
+
dataModels: entry.dataModels,
|
|
1256
|
+
tasks: entry.tasks,
|
|
1257
|
+
enums: entry.enums,
|
|
1258
|
+
constants: entry.constants,
|
|
1259
|
+
endpointModelUsage: entry.endpointModelUsage
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
const fileGraph = new Map();
|
|
1264
|
+
for (const file of codeFiles) {
|
|
1265
|
+
ensureNode(fileGraph, file);
|
|
1266
|
+
}
|
|
1267
|
+
const moduleGraph = [];
|
|
1268
|
+
const moduleGraphKeys = new Set();
|
|
1269
|
+
const fileUsedSymbols = new Map();
|
|
1270
|
+
const fileWildcardUse = new Set();
|
|
1271
|
+
const fileExportDetails = new Map();
|
|
1272
|
+
const pythonEndpointModelUsageByFile = new Map();
|
|
1273
|
+
const recordUsage = (targetFile, symbols, wildcard) => {
|
|
1274
|
+
if (wildcard) {
|
|
1275
|
+
fileWildcardUse.add(targetFile);
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
if (symbols.length === 0) {
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const current = fileUsedSymbols.get(targetFile) ?? new Set();
|
|
1282
|
+
for (const symbol of symbols) {
|
|
1283
|
+
current.add(symbol);
|
|
1284
|
+
}
|
|
1285
|
+
fileUsedSymbols.set(targetFile, current);
|
|
1286
|
+
};
|
|
1287
|
+
for (const file of codeFiles) {
|
|
1288
|
+
const absoluteFile = path.join(baseRoot, file);
|
|
1289
|
+
let content = "";
|
|
1290
|
+
try {
|
|
1291
|
+
content = await fs.readFile(absoluteFile, "utf8");
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
fileContents.set(file, content);
|
|
1297
|
+
const ext = path.extname(file);
|
|
1298
|
+
let importUsages = [];
|
|
1299
|
+
let exports = [];
|
|
1300
|
+
let exportDetails = [];
|
|
1301
|
+
const moduleId = fileToModule.get(file) ?? "root";
|
|
1302
|
+
if (ext === ".py") {
|
|
1303
|
+
const cachedPython = pythonResultsByFile.get(file);
|
|
1304
|
+
if (cachedPython) {
|
|
1305
|
+
importUsages = cachedPython.importUsages;
|
|
1306
|
+
exports = cachedPython.exports;
|
|
1307
|
+
exportDetails = cachedPython.exportDetails;
|
|
1308
|
+
pythonEndpointModelUsageByFile.set(file, cachedPython.endpointModelUsage);
|
|
1309
|
+
for (const endpoint of cachedPython.endpoints) {
|
|
1310
|
+
const signature = `${endpoint.method} ${endpoint.path}`;
|
|
1311
|
+
const id = endpointKeys.has(signature) ? `${signature} (${file})` : signature;
|
|
1312
|
+
endpointKeys.add(id);
|
|
1313
|
+
endpoints.push({
|
|
1314
|
+
...endpoint,
|
|
1315
|
+
id,
|
|
1316
|
+
file,
|
|
1317
|
+
module: moduleId
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
for (const model of cachedPython.dataModels) {
|
|
1321
|
+
const key = `${model.framework}|${model.name}|${model.file}`;
|
|
1322
|
+
if (dataModelKeys.has(key)) {
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
dataModelKeys.add(key);
|
|
1326
|
+
dataModels.push(model);
|
|
1327
|
+
}
|
|
1328
|
+
tasks.push(...cachedPython.tasks);
|
|
1329
|
+
enums.push(...cachedPython.enums);
|
|
1330
|
+
constants.push(...cachedPython.constants);
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
importUsages = extractPythonImportUsages(content);
|
|
1334
|
+
exports = extractPythonExports(content);
|
|
1335
|
+
for (const endpoint of extractPythonEndpoints(content)) {
|
|
1336
|
+
const signature = `${endpoint.method} ${endpoint.path}`;
|
|
1337
|
+
const id = endpointKeys.has(signature) ? `${signature} (${file})` : signature;
|
|
1338
|
+
endpointKeys.add(id);
|
|
1339
|
+
endpoints.push({
|
|
1340
|
+
id,
|
|
1341
|
+
method: endpoint.method,
|
|
1342
|
+
path: endpoint.path,
|
|
1343
|
+
handler: endpoint.handler,
|
|
1344
|
+
file,
|
|
1345
|
+
module: moduleId,
|
|
1346
|
+
request_schema: endpoint.request_schema,
|
|
1347
|
+
response_schema: endpoint.response_schema,
|
|
1348
|
+
service_calls: endpoint.service_calls,
|
|
1349
|
+
ai_operations: endpoint.ai_operations
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
for (const task of extractCeleryTasks(content)) {
|
|
1353
|
+
tasks.push({
|
|
1354
|
+
name: task.name,
|
|
1355
|
+
file,
|
|
1356
|
+
kind: "celery",
|
|
1357
|
+
queue: task.queue ?? null,
|
|
1358
|
+
schedule: null
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
const modelCandidates = [
|
|
1362
|
+
...extractSqlAlchemyModels(content, file),
|
|
1363
|
+
...extractDjangoModels(content, file)
|
|
1364
|
+
];
|
|
1365
|
+
for (const model of modelCandidates) {
|
|
1366
|
+
const key = `${model.framework}|${model.name}|${model.file}`;
|
|
1367
|
+
if (dataModelKeys.has(key)) {
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
dataModelKeys.add(key);
|
|
1371
|
+
dataModels.push(model);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
else {
|
|
1376
|
+
const stat = await fs.stat(absoluteFile);
|
|
1377
|
+
const hash = hashContent(content);
|
|
1378
|
+
const cached = cache.files[absoluteFile];
|
|
1379
|
+
const canReuse = cached &&
|
|
1380
|
+
cached.language === "javascript" &&
|
|
1381
|
+
cached.hash === hash;
|
|
1382
|
+
if (canReuse) {
|
|
1383
|
+
cached.mtime = stat.mtimeMs;
|
|
1384
|
+
importUsages = cached.importUsages;
|
|
1385
|
+
exports = cached.exports;
|
|
1386
|
+
exportDetails = cached.exportDetails;
|
|
1387
|
+
for (const endpoint of cached.endpoints) {
|
|
1388
|
+
const signature = `${endpoint.method} ${endpoint.path}`;
|
|
1389
|
+
const id = endpointKeys.has(signature) ? `${signature} (${file})` : signature;
|
|
1390
|
+
endpointKeys.add(id);
|
|
1391
|
+
endpoints.push({
|
|
1392
|
+
...endpoint,
|
|
1393
|
+
id,
|
|
1394
|
+
file,
|
|
1395
|
+
module: moduleId
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
for (const model of cached.dataModels) {
|
|
1399
|
+
const key = `${model.framework}|${model.name}|${model.file}`;
|
|
1400
|
+
if (dataModelKeys.has(key)) {
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
dataModelKeys.add(key);
|
|
1404
|
+
dataModels.push(model);
|
|
1405
|
+
}
|
|
1406
|
+
tasks.push(...cached.tasks);
|
|
1407
|
+
enums.push(...cached.enums);
|
|
1408
|
+
constants.push(...cached.constants);
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
const parsed = parseJsFile(content, file);
|
|
1412
|
+
importUsages = parsed.usages;
|
|
1413
|
+
exports = parsed.exports;
|
|
1414
|
+
exportDetails = parsed.exportDetails;
|
|
1415
|
+
const fileEndpoints = [];
|
|
1416
|
+
for (const endpoint of extractJsEndpoints(content, file)) {
|
|
1417
|
+
const signature = `${endpoint.method} ${endpoint.path}`;
|
|
1418
|
+
const id = endpointKeys.has(signature) ? `${signature} (${file})` : signature;
|
|
1419
|
+
endpointKeys.add(id);
|
|
1420
|
+
const entry = {
|
|
1421
|
+
id,
|
|
1422
|
+
method: endpoint.method,
|
|
1423
|
+
path: endpoint.path,
|
|
1424
|
+
handler: endpoint.handler,
|
|
1425
|
+
file,
|
|
1426
|
+
module: moduleId,
|
|
1427
|
+
request_schema: endpoint.request_schema,
|
|
1428
|
+
response_schema: endpoint.response_schema,
|
|
1429
|
+
service_calls: endpoint.service_calls,
|
|
1430
|
+
ai_operations: endpoint.ai_operations
|
|
1431
|
+
};
|
|
1432
|
+
fileEndpoints.push(entry);
|
|
1433
|
+
endpoints.push(entry);
|
|
1434
|
+
}
|
|
1435
|
+
const fileEnums = extractJsEnums(content, file);
|
|
1436
|
+
const fileConstants = extractJsConstants(content, file);
|
|
1437
|
+
enums.push(...fileEnums);
|
|
1438
|
+
constants.push(...fileConstants);
|
|
1439
|
+
cache.files[absoluteFile] = {
|
|
1440
|
+
hash,
|
|
1441
|
+
mtime: stat.mtimeMs,
|
|
1442
|
+
language: "javascript",
|
|
1443
|
+
importUsages,
|
|
1444
|
+
exports,
|
|
1445
|
+
exportDetails,
|
|
1446
|
+
endpoints: fileEndpoints.map((endpoint) => ({
|
|
1447
|
+
...endpoint,
|
|
1448
|
+
id: "",
|
|
1449
|
+
file
|
|
1450
|
+
})),
|
|
1451
|
+
dataModels: [],
|
|
1452
|
+
tasks: [],
|
|
1453
|
+
enums: fileEnums,
|
|
1454
|
+
constants: fileConstants,
|
|
1455
|
+
endpointModelUsage: []
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (exports.length > 0) {
|
|
1460
|
+
fileExports.set(file, exports);
|
|
1461
|
+
fileExportDetails.set(file, exportDetails);
|
|
1462
|
+
}
|
|
1463
|
+
for (const usage of importUsages) {
|
|
1464
|
+
const resolved = ext === ".py"
|
|
1465
|
+
? await resolvePythonImport(absoluteFile, usage.specifier, root, absoluteImportRoots)
|
|
1466
|
+
: await resolveJsImport(absoluteFile, usage.specifier);
|
|
1467
|
+
if (!resolved) {
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
const resolvedRel = toPosix(path.relative(baseRoot, resolved));
|
|
1471
|
+
if (!knownFiles.has(resolvedRel)) {
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
addEdge(fileGraph, file, resolvedRel);
|
|
1475
|
+
recordUsage(resolvedRel, usage.symbols, usage.wildcard);
|
|
1476
|
+
const targetModule = fileToModule.get(resolvedRel);
|
|
1477
|
+
const sourceModule = fileToModule.get(file);
|
|
1478
|
+
if (!targetModule || !sourceModule || targetModule === sourceModule) {
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
moduleImports.get(sourceModule)?.add(targetModule);
|
|
1482
|
+
const edgeKey = `${sourceModule}|${targetModule}|${file}`;
|
|
1483
|
+
if (!moduleGraphKeys.has(edgeKey)) {
|
|
1484
|
+
moduleGraphKeys.add(edgeKey);
|
|
1485
|
+
moduleGraph.push({ from: sourceModule, to: targetModule, file });
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
for (const module of modules) {
|
|
1490
|
+
module.imports = Array.from(moduleImports.get(module.id) ?? []).sort((a, b) => a.localeCompare(b));
|
|
1491
|
+
}
|
|
1492
|
+
moduleGraph.sort((a, b) => {
|
|
1493
|
+
const from = a.from.localeCompare(b.from);
|
|
1494
|
+
if (from !== 0)
|
|
1495
|
+
return from;
|
|
1496
|
+
const to = a.to.localeCompare(b.to);
|
|
1497
|
+
if (to !== 0)
|
|
1498
|
+
return to;
|
|
1499
|
+
return a.file.localeCompare(b.file);
|
|
1500
|
+
});
|
|
1501
|
+
const fileGraphEdges = [];
|
|
1502
|
+
for (const [from, targets] of fileGraph) {
|
|
1503
|
+
for (const to of targets) {
|
|
1504
|
+
fileGraphEdges.push({ from, to });
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
fileGraphEdges.sort((a, b) => {
|
|
1508
|
+
const from = a.from.localeCompare(b.from);
|
|
1509
|
+
if (from !== 0)
|
|
1510
|
+
return from;
|
|
1511
|
+
return a.to.localeCompare(b.to);
|
|
1512
|
+
});
|
|
1513
|
+
const moduleAdjacency = new Map();
|
|
1514
|
+
for (const module of modules) {
|
|
1515
|
+
ensureNode(moduleAdjacency, module.id);
|
|
1516
|
+
}
|
|
1517
|
+
for (const edge of moduleGraph) {
|
|
1518
|
+
addEdge(moduleAdjacency, edge.from, edge.to);
|
|
1519
|
+
}
|
|
1520
|
+
const entrypoints = await detectEntrypoints(root, baseRoot);
|
|
1521
|
+
const fileInbound = inboundCounts(fileGraph, codeFiles);
|
|
1522
|
+
const moduleUsage = {};
|
|
1523
|
+
const orphanFiles = codeFiles
|
|
1524
|
+
.filter((file) => (fileInbound.get(file) ?? 0) === 0)
|
|
1525
|
+
.filter((file) => !entrypoints.has(file))
|
|
1526
|
+
.sort((a, b) => a.localeCompare(b));
|
|
1527
|
+
const moduleInbound = new Map();
|
|
1528
|
+
for (const module of modules) {
|
|
1529
|
+
moduleInbound.set(module.id, 0);
|
|
1530
|
+
}
|
|
1531
|
+
for (const neighbors of fileGraph.values()) {
|
|
1532
|
+
for (const neighbor of neighbors) {
|
|
1533
|
+
const targetModule = fileToModule.get(neighbor);
|
|
1534
|
+
if (!targetModule) {
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
moduleInbound.set(targetModule, (moduleInbound.get(targetModule) ?? 0) + 1);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
for (const module of modules) {
|
|
1541
|
+
const inbound = moduleInbound.get(module.id) ?? 0;
|
|
1542
|
+
moduleUsage[module.id] = inbound;
|
|
1543
|
+
}
|
|
1544
|
+
const moduleEntrypoints = new Map();
|
|
1545
|
+
for (const module of modules) {
|
|
1546
|
+
const hasEntry = Array.from(entrypoints).some((entry) => {
|
|
1547
|
+
const modulePath = module.path;
|
|
1548
|
+
return entry === modulePath || entry.startsWith(`${modulePath}/`);
|
|
1549
|
+
});
|
|
1550
|
+
moduleEntrypoints.set(module.id, hasEntry);
|
|
1551
|
+
}
|
|
1552
|
+
const orphanModules = modules
|
|
1553
|
+
.filter((module) => (moduleInbound.get(module.id) ?? 0) === 0)
|
|
1554
|
+
.filter((module) => (module.endpoints ?? []).length === 0)
|
|
1555
|
+
.filter((module) => !moduleEntrypoints.get(module.id))
|
|
1556
|
+
.map((module) => module.id)
|
|
1557
|
+
.sort((a, b) => a.localeCompare(b));
|
|
1558
|
+
const circularDependencies = findCycles(moduleAdjacency);
|
|
1559
|
+
const unusedExports = [];
|
|
1560
|
+
for (const [file, symbols] of fileExports) {
|
|
1561
|
+
if (entrypoints.has(file)) {
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
if (fileWildcardUse.has(file)) {
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
const used = fileUsedSymbols.get(file) ?? new Set();
|
|
1568
|
+
for (const symbol of symbols) {
|
|
1569
|
+
if (!used.has(symbol)) {
|
|
1570
|
+
unusedExports.push({ file, symbol });
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
unusedExports.sort((a, b) => {
|
|
1575
|
+
const fileCmp = a.file.localeCompare(b.file);
|
|
1576
|
+
if (fileCmp !== 0)
|
|
1577
|
+
return fileCmp;
|
|
1578
|
+
return a.symbol.localeCompare(b.symbol);
|
|
1579
|
+
});
|
|
1580
|
+
for (const module of modules) {
|
|
1581
|
+
const exports = [];
|
|
1582
|
+
for (const file of module.files) {
|
|
1583
|
+
const symbols = fileExports.get(file);
|
|
1584
|
+
if (symbols && symbols.length > 0) {
|
|
1585
|
+
exports.push({
|
|
1586
|
+
file,
|
|
1587
|
+
symbols,
|
|
1588
|
+
exports: fileExportDetails.get(file) ?? symbols.map((name) => ({
|
|
1589
|
+
name,
|
|
1590
|
+
kind: name === "default" ? "default" : "named"
|
|
1591
|
+
}))
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
module.exports = exports;
|
|
1596
|
+
}
|
|
1597
|
+
for (const module of modules) {
|
|
1598
|
+
module.layer = classifyLayer(moduleInbound.get(module.id) ?? 0, module.imports.length);
|
|
1599
|
+
}
|
|
1600
|
+
const accessForMethod = (method) => {
|
|
1601
|
+
const upper = method.toUpperCase();
|
|
1602
|
+
if (upper === "GET" || upper === "HEAD" || upper === "OPTIONS") {
|
|
1603
|
+
return "read";
|
|
1604
|
+
}
|
|
1605
|
+
if (upper === "POST" || upper === "PUT" || upper === "PATCH" || upper === "DELETE") {
|
|
1606
|
+
return "write";
|
|
1607
|
+
}
|
|
1608
|
+
if (upper === "ANY") {
|
|
1609
|
+
return "read_write";
|
|
1610
|
+
}
|
|
1611
|
+
return "unknown";
|
|
1612
|
+
};
|
|
1613
|
+
const endpointModelUsage = [];
|
|
1614
|
+
const cachedPythonUsages = Array.from(pythonEndpointModelUsageByFile.entries());
|
|
1615
|
+
if (cachedPythonUsages.length > 0) {
|
|
1616
|
+
for (const [relativeFile, usages] of cachedPythonUsages) {
|
|
1617
|
+
for (const usage of usages) {
|
|
1618
|
+
const matches = endpoints.filter((endpoint) => endpoint.file === relativeFile &&
|
|
1619
|
+
endpoint.handler === usage.handler);
|
|
1620
|
+
for (const endpoint of matches) {
|
|
1621
|
+
const models = usage.models.map((name) => ({
|
|
1622
|
+
name,
|
|
1623
|
+
access: accessForMethod(endpoint.method)
|
|
1624
|
+
}));
|
|
1625
|
+
endpointModelUsage.push({
|
|
1626
|
+
endpoint_id: endpoint.id,
|
|
1627
|
+
endpoint: `${endpoint.method} ${endpoint.path}`,
|
|
1628
|
+
models
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
else if (dataModels.length > 0 && endpoints.length > 0) {
|
|
1635
|
+
const modelNames = dataModels.map((model) => model.name);
|
|
1636
|
+
const modelPatterns = modelNames.map((name) => new RegExp(`\\b${name}\\b`));
|
|
1637
|
+
for (const endpoint of endpoints) {
|
|
1638
|
+
const content = fileContents.get(endpoint.file) ?? "";
|
|
1639
|
+
const modelsForEndpoint = [];
|
|
1640
|
+
modelPatterns.forEach((pattern, index) => {
|
|
1641
|
+
if (pattern.test(content)) {
|
|
1642
|
+
modelsForEndpoint.push({
|
|
1643
|
+
name: modelNames[index],
|
|
1644
|
+
access: accessForMethod(endpoint.method)
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
});
|
|
1648
|
+
if (modelsForEndpoint.length > 0) {
|
|
1649
|
+
endpointModelUsage.push({
|
|
1650
|
+
endpoint_id: endpoint.id,
|
|
1651
|
+
endpoint: `${endpoint.method} ${endpoint.path}`,
|
|
1652
|
+
models: modelsForEndpoint
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
const { duplicateFunctions, similarFunctions } = await findDuplicateFunctions({
|
|
1658
|
+
files: codeFiles,
|
|
1659
|
+
baseRoot,
|
|
1660
|
+
fileContents
|
|
1661
|
+
});
|
|
1662
|
+
const testCoverage = computeTestCoverage(Array.from(knownFiles));
|
|
1663
|
+
await saveBackendExtractionCache(cachePath, cache);
|
|
1664
|
+
// --- 6. Extract Tests Natively using Universal Adapters ---
|
|
1665
|
+
const tests = [];
|
|
1666
|
+
for (const relativeFile of codeFiles) {
|
|
1667
|
+
if (!relativeFile.includes("test") && !relativeFile.includes("spec") && !relativeFile.includes("Test"))
|
|
1668
|
+
continue;
|
|
1669
|
+
const absoluteFile = path.join(baseRoot, relativeFile);
|
|
1670
|
+
try {
|
|
1671
|
+
const content = await fs.readFile(absoluteFile, "utf8");
|
|
1672
|
+
const adapter = getAdapterForFile(relativeFile);
|
|
1673
|
+
if (adapter && adapter.queries.tests) {
|
|
1674
|
+
const result = runAdapter(adapter, relativeFile, content);
|
|
1675
|
+
tests.push(...result.tests);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
catch {
|
|
1679
|
+
// gracefully ignore unparseable or inaccessible test files
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return {
|
|
1683
|
+
modules,
|
|
1684
|
+
moduleGraph,
|
|
1685
|
+
fileGraph: fileGraphEdges,
|
|
1686
|
+
endpoints,
|
|
1687
|
+
dataModels,
|
|
1688
|
+
endpointModelUsage,
|
|
1689
|
+
tasks,
|
|
1690
|
+
circularDependencies,
|
|
1691
|
+
orphanModules,
|
|
1692
|
+
orphanFiles,
|
|
1693
|
+
moduleUsage,
|
|
1694
|
+
unusedExports,
|
|
1695
|
+
unusedEndpoints: [],
|
|
1696
|
+
entrypoints: Array.from(entrypoints).sort((a, b) => a.localeCompare(b)),
|
|
1697
|
+
duplicateFunctions,
|
|
1698
|
+
similarFunctions,
|
|
1699
|
+
enums,
|
|
1700
|
+
constants,
|
|
1701
|
+
testCoverage,
|
|
1702
|
+
tests
|
|
1703
|
+
};
|
|
1704
|
+
}
|