@schemyx/mcp 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -2
- package/dist/backend-client.d.ts +1 -17
- package/dist/backend-client.js +15 -91
- package/dist/backend-client.js.map +1 -1
- package/dist/client/backend-client.d.ts +17 -0
- package/dist/client/backend-client.js +97 -0
- package/dist/client/backend-client.js.map +1 -0
- package/dist/client/local-theme-source.d.ts +19 -0
- package/dist/client/local-theme-source.js +737 -0
- package/dist/client/local-theme-source.js.map +1 -0
- package/dist/client/mcp-client.d.ts +15 -0
- package/dist/client/mcp-client.js +46 -0
- package/dist/client/mcp-client.js.map +1 -0
- package/dist/codebase-scanner/backend.d.ts +12 -0
- package/dist/codebase-scanner/backend.js +814 -0
- package/dist/codebase-scanner/backend.js.map +1 -0
- package/dist/codebase-scanner/bundle.d.ts +81 -0
- package/dist/codebase-scanner/bundle.js +2177 -0
- package/dist/codebase-scanner/bundle.js.map +1 -0
- package/dist/codebase-scanner/constants.d.ts +26 -0
- package/dist/codebase-scanner/constants.js +230 -0
- package/dist/codebase-scanner/constants.js.map +1 -0
- package/dist/codebase-scanner/extractors.d.ts +187 -0
- package/dist/codebase-scanner/extractors.js +2600 -0
- package/dist/codebase-scanner/extractors.js.map +1 -0
- package/dist/codebase-scanner/files.d.ts +16 -0
- package/dist/codebase-scanner/files.js +233 -0
- package/dist/codebase-scanner/files.js.map +1 -0
- package/dist/codebase-scanner/index.d.ts +217 -0
- package/dist/codebase-scanner/index.js +387 -0
- package/dist/codebase-scanner/index.js.map +1 -0
- package/dist/codebase-scanner/recipes.d.ts +74 -0
- package/dist/codebase-scanner/recipes.js +585 -0
- package/dist/codebase-scanner/recipes.js.map +1 -0
- package/dist/codebase-scanner/storage.d.ts +19 -0
- package/dist/codebase-scanner/storage.js +103 -0
- package/dist/codebase-scanner/storage.js.map +1 -0
- package/dist/codebase-scanner/types.d.ts +522 -0
- package/dist/codebase-scanner/types.js +3 -0
- package/dist/codebase-scanner/types.js.map +1 -0
- package/dist/codebase-scanner/utils.d.ts +37 -0
- package/dist/codebase-scanner/utils.js +259 -0
- package/dist/codebase-scanner/utils.js.map +1 -0
- package/dist/codebase-scanner.d.ts +1 -0
- package/dist/codebase-scanner.js +18 -0
- package/dist/codebase-scanner.js.map +1 -0
- package/dist/config.d.ts +1 -2
- package/dist/config.js +15 -37
- package/dist/config.js.map +1 -1
- package/dist/local-theme-source.d.ts +1 -0
- package/dist/local-theme-source.js +18 -0
- package/dist/local-theme-source.js.map +1 -0
- package/dist/main.js +3 -3
- package/dist/main.js.map +1 -1
- package/dist/mcp-client.d.ts +1 -0
- package/dist/mcp-client.js +18 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/prompts.d.ts +1 -7
- package/dist/prompts.js +15 -52
- package/dist/prompts.js.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +163 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/prompts.d.ts +7 -0
- package/dist/server/prompts.js +55 -0
- package/dist/server/prompts.js.map +1 -0
- package/dist/server/tool-definitions.d.ts +22 -0
- package/dist/server/tool-definitions.js +531 -0
- package/dist/server/tool-definitions.js.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +33 -0
- package/dist/server.js.map +1 -1
- package/dist/shared/config.d.ts +2 -0
- package/dist/shared/config.js +54 -0
- package/dist/shared/config.js.map +1 -0
- package/dist/shared/text.d.ts +14 -0
- package/dist/shared/text.js +33 -0
- package/dist/shared/text.js.map +1 -0
- package/dist/shared/types.d.ts +118 -0
- package/dist/shared/types.js +3 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/shared/uri.d.ts +6 -0
- package/dist/shared/uri.js +24 -0
- package/dist/shared/uri.js.map +1 -0
- package/dist/style-recipes.d.ts +1 -0
- package/dist/style-recipes.js +18 -0
- package/dist/style-recipes.js.map +1 -0
- package/dist/text.d.ts +1 -14
- package/dist/text.js +15 -30
- package/dist/text.js.map +1 -1
- package/dist/theme/style-recipes.d.ts +26 -0
- package/dist/theme/style-recipes.js +129 -0
- package/dist/theme/style-recipes.js.map +1 -0
- package/dist/tool-definitions.d.ts +1 -11
- package/dist/tool-definitions.js +15 -127
- package/dist/tool-definitions.js.map +1 -1
- package/dist/types.d.ts +1 -106
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -1
- package/dist/uri.d.ts +1 -6
- package/dist/uri.js +15 -21
- package/dist/uri.js.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractBackendEndpointContracts = extractBackendEndpointContracts;
|
|
4
|
+
exports.extractBackendOperations = extractBackendOperations;
|
|
5
|
+
exports.extractBackendSideEffects = extractBackendSideEffects;
|
|
6
|
+
exports.extractBackendErrors = extractBackendErrors;
|
|
7
|
+
exports.extractBackendLogging = extractBackendLogging;
|
|
8
|
+
exports.detectBackendFramework = detectBackendFramework;
|
|
9
|
+
exports.backendReceiverToClassName = backendReceiverToClassName;
|
|
10
|
+
const extractors_1 = require("./extractors");
|
|
11
|
+
const utils_1 = require("./utils");
|
|
12
|
+
const httpMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];
|
|
13
|
+
const mutatingMethods = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
14
|
+
function extractBackendEndpointContracts(relPath, content) {
|
|
15
|
+
return uniqueEndpointContracts([
|
|
16
|
+
...extractNestEndpointContracts(relPath, content),
|
|
17
|
+
...extractNextEndpointContracts(relPath, content),
|
|
18
|
+
...extractRouterEndpointContracts(relPath, content),
|
|
19
|
+
...extractLaravelEndpointContracts(relPath, content),
|
|
20
|
+
...extractDjangoEndpointContracts(relPath, content),
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
function extractBackendOperations(relPath, content, owners) {
|
|
24
|
+
if (!owners.controllers.length && !owners.services.length && !owners.modules.length) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const framework = detectBackendFramework(relPath, content);
|
|
28
|
+
const operations = [];
|
|
29
|
+
const ownerNames = (0, utils_1.unique)([...owners.controllers, ...owners.services, ...owners.modules]);
|
|
30
|
+
for (const owner of ownerNames) {
|
|
31
|
+
const ownerKind = owners.controllers.includes(owner)
|
|
32
|
+
? 'controller'
|
|
33
|
+
: owners.services.includes(owner)
|
|
34
|
+
? 'service'
|
|
35
|
+
: owners.modules.includes(owner)
|
|
36
|
+
? 'module'
|
|
37
|
+
: 'unknown';
|
|
38
|
+
const classBlock = readClassBody(content, owner);
|
|
39
|
+
if (!classBlock) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
for (const method of readClassMethods(classBlock.body, classBlock.offset)) {
|
|
43
|
+
if (method.name === 'constructor') {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const serviceCalls = extractBackendServiceCalls(method.body, content, method.index);
|
|
47
|
+
const sideEffects = extractBackendSideEffects(method.body, content, method.index);
|
|
48
|
+
const errors = extractBackendErrors(method.body, content, method.index);
|
|
49
|
+
const logging = extractBackendLogging(method.body, content, method.index);
|
|
50
|
+
operations.push({
|
|
51
|
+
framework,
|
|
52
|
+
owner,
|
|
53
|
+
ownerKind,
|
|
54
|
+
name: method.name,
|
|
55
|
+
signature: `${method.name}(${compactWhitespace(method.params)})`,
|
|
56
|
+
line: (0, utils_1.lineNumberAt)(content, method.index),
|
|
57
|
+
params: parseBackendParams(method.params),
|
|
58
|
+
returnType: cleanType(method.returnType),
|
|
59
|
+
serviceCalls,
|
|
60
|
+
repositoryCalls: serviceCalls.filter((call) => call.kind === 'repository'),
|
|
61
|
+
sideEffects,
|
|
62
|
+
errors,
|
|
63
|
+
logging,
|
|
64
|
+
risk: backendRiskLabels({
|
|
65
|
+
method: ownerKind === 'controller' ? undefined : undefined,
|
|
66
|
+
auth: undefined,
|
|
67
|
+
sideEffects,
|
|
68
|
+
errors,
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return operations.slice(0, 120);
|
|
74
|
+
}
|
|
75
|
+
function extractBackendSideEffects(content, fullContent = content, offset = 0) {
|
|
76
|
+
const effects = [];
|
|
77
|
+
addSideEffects(effects, content, fullContent, offset, /\bthis\.([A-Za-z_$][\w$]*(?:Repository|Repo|Store))\.(create|update|upsert|delete|remove|save|insert)[A-Za-z_$\w]*/g, 'database-write', 'high');
|
|
78
|
+
addSideEffects(effects, content, fullContent, offset, /\b(?:prisma|db|knex|sequelize|mongoose)\.[A-Za-z_$][\w$]*\.(create|update|upsert|delete|deleteMany|insert|save)\b/g, 'database-write', 'high');
|
|
79
|
+
addSideEffects(effects, content, fullContent, offset, /\bthis\.([A-Za-z_$][\w$]*(?:Email|Mailer|Mail|Notification)[A-Za-z_$\w]*)\.(send|notify|deliver)[A-Za-z_$\w]*/g, 'email', 'high');
|
|
80
|
+
addSideEffects(effects, content, fullContent, offset, /\b(?:queue|dispatch|enqueue|schedule)\((['"`]?)([A-Za-z0-9_.:-]+)?\1/g, 'queue', 'medium');
|
|
81
|
+
addSideEffects(effects, content, fullContent, offset, /\b(?:emit|publish|broadcast|subscribe)\(\s*['"`]([^'"`]+)['"`]/g, 'event', 'medium');
|
|
82
|
+
addSideEffects(effects, content, fullContent, offset, /\bresponse\.(cookie|clearCookie)\(/g, 'cookie', 'high');
|
|
83
|
+
addSideEffects(effects, content, fullContent, offset, /\b(?:response|res)\.(?:set|setHeader|header)\(/g, 'header', 'medium');
|
|
84
|
+
addSideEffects(effects, content, fullContent, offset, /\b(?:fetch|axios\.(?:get|post|put|patch|delete)|httpService\.(?:get|post|put|patch|delete))\(/g, 'external-api', 'medium');
|
|
85
|
+
addSideEffects(effects, content, fullContent, offset, /\b(?:cache|redis)\.(?:set|del|delete|expire|incr|decr)\(/g, 'cache', 'medium');
|
|
86
|
+
return uniqueSideEffects(effects).slice(0, 80);
|
|
87
|
+
}
|
|
88
|
+
function extractBackendErrors(content, fullContent = content, offset = 0) {
|
|
89
|
+
const errors = [];
|
|
90
|
+
for (const match of content.matchAll(/\bthrow\s+new\s+([A-Za-z_$][\w$]*)(?:\(\s*(['"`])([^'"`]*)\2)?/g)) {
|
|
91
|
+
errors.push({
|
|
92
|
+
kind: match[1],
|
|
93
|
+
...(match[3] ? { message: match[3].slice(0, 180) } : {}),
|
|
94
|
+
statusCode: exceptionStatusCode(match[1]),
|
|
95
|
+
line: (0, utils_1.lineNumberAt)(fullContent, offset + match.index),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return errors.slice(0, 80);
|
|
99
|
+
}
|
|
100
|
+
function extractBackendLogging(content, fullContent = content, offset = 0) {
|
|
101
|
+
const logs = [];
|
|
102
|
+
for (const match of content.matchAll(/\b(?:this\.)?logger\.(log|warn|error|debug|verbose)\(\s*(?:`([^`]*)`|['"]([^'"]*)['"])?/g)) {
|
|
103
|
+
logs.push({
|
|
104
|
+
level: match[1],
|
|
105
|
+
message: (match[2] ?? match[3])?.replace(/\s+/g, ' ').trim().slice(0, 180),
|
|
106
|
+
line: (0, utils_1.lineNumberAt)(fullContent, offset + match.index),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
return logs.slice(0, 80);
|
|
110
|
+
}
|
|
111
|
+
function detectBackendFramework(relPath, content) {
|
|
112
|
+
if (/@Controller\(|@Injectable\(|@Module\(/.test(content) || /@nestjs\//.test(content)) {
|
|
113
|
+
return 'nestjs';
|
|
114
|
+
}
|
|
115
|
+
if ((0, extractors_1.isNextRouteFile)(relPath)) {
|
|
116
|
+
return 'nextjs-route-handler';
|
|
117
|
+
}
|
|
118
|
+
if (/\b(?:router|app|server)\.(?:get|post|put|patch|delete)\(/i.test(content)) {
|
|
119
|
+
return 'node-router';
|
|
120
|
+
}
|
|
121
|
+
if (/Route::(?:get|post|put|patch|delete|options|any)\(/i.test(content)) {
|
|
122
|
+
return 'laravel';
|
|
123
|
+
}
|
|
124
|
+
if (/\bpath\(\s*['"]|url\(r?['"]/.test(content)) {
|
|
125
|
+
return 'django';
|
|
126
|
+
}
|
|
127
|
+
if (/Rails\.application\.routes|^\s*(?:get|post|patch|put|delete)\s+['"]/m.test(content)) {
|
|
128
|
+
return 'rails';
|
|
129
|
+
}
|
|
130
|
+
return 'generic-backend';
|
|
131
|
+
}
|
|
132
|
+
function extractNestEndpointContracts(relPath, content) {
|
|
133
|
+
const contracts = [];
|
|
134
|
+
const controller = readNestController(content);
|
|
135
|
+
if (!controller) {
|
|
136
|
+
return contracts;
|
|
137
|
+
}
|
|
138
|
+
const methodPattern = /((?:\s*@[A-Za-z_$][\w$]*(?:\([^)]*\))?\s*)+)\s*(?:public\s+|private\s+|protected\s+)?(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(([\s\S]*?)\)\s*(?::\s*([^{]+))?\s*\{/g;
|
|
139
|
+
for (const match of content.matchAll(methodPattern)) {
|
|
140
|
+
const decorators = readDecorators(match[1]);
|
|
141
|
+
const routeDecorators = decorators.filter((decorator) => /^@(Get|Post|Put|Patch|Delete|Options|Head)\b/.test(decorator));
|
|
142
|
+
if (!routeDecorators.length) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const openIndex = match.index + match[0].lastIndexOf('{');
|
|
146
|
+
const body = (0, extractors_1.readBalancedBlock)(content, openIndex) ?? '';
|
|
147
|
+
const bodyContexts = [
|
|
148
|
+
{ body, offset: openIndex },
|
|
149
|
+
...readLocalHelperMethodBodies(content, controller.name, body),
|
|
150
|
+
];
|
|
151
|
+
const params = parseBackendParams(match[3]);
|
|
152
|
+
const auth = authPolicyFromDecorators(decorators, body);
|
|
153
|
+
const serviceCalls = extractBackendServiceCalls(body, content, openIndex);
|
|
154
|
+
const sideEffects = bodyContexts.flatMap((context) => extractBackendSideEffects(context.body, content, context.offset));
|
|
155
|
+
const errors = bodyContexts.flatMap((context) => extractBackendErrors(context.body, content, context.offset));
|
|
156
|
+
const logging = bodyContexts.flatMap((context) => extractBackendLogging(context.body, content, context.offset));
|
|
157
|
+
for (const decorator of routeDecorators) {
|
|
158
|
+
const route = readNestRouteDecorator(decorator);
|
|
159
|
+
const method = route.method;
|
|
160
|
+
const fullPath = joinRoutePaths(controller.basePath, route.path);
|
|
161
|
+
contracts.push({
|
|
162
|
+
framework: 'nestjs',
|
|
163
|
+
method,
|
|
164
|
+
path: route.path,
|
|
165
|
+
fullPath,
|
|
166
|
+
handler: `${method} ${(0, extractors_1.normalizeRoutePath)(fullPath)}`,
|
|
167
|
+
handlerName: match[2],
|
|
168
|
+
controller: controller.name,
|
|
169
|
+
line: (0, utils_1.lineNumberAt)(content, match.index),
|
|
170
|
+
decorators,
|
|
171
|
+
params,
|
|
172
|
+
request: requestContractFromParams(params),
|
|
173
|
+
response: {
|
|
174
|
+
type: cleanType(match[4]),
|
|
175
|
+
statusCodes: statusCodesFromDecorators(decorators, method),
|
|
176
|
+
returns: extractReturnShapes(body),
|
|
177
|
+
},
|
|
178
|
+
auth,
|
|
179
|
+
validation: validationContractFromParams(params, decorators),
|
|
180
|
+
serviceCalls,
|
|
181
|
+
repositoryCalls: serviceCalls.filter((call) => call.kind === 'repository'),
|
|
182
|
+
sideEffects,
|
|
183
|
+
errors,
|
|
184
|
+
logging,
|
|
185
|
+
risk: backendRiskLabels({ method, auth, sideEffects, errors }),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return contracts;
|
|
190
|
+
}
|
|
191
|
+
function extractNextEndpointContracts(relPath, content) {
|
|
192
|
+
if (!(0, extractors_1.isNextRouteFile)(relPath)) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
const routePath = (0, extractors_1.toNextRoutePath)(relPath);
|
|
196
|
+
const contracts = [];
|
|
197
|
+
for (const method of httpMethods) {
|
|
198
|
+
const pattern = new RegExp(`export\\s+(?:async\\s+)?(?:function\\s+${method}\\s*\\(([^)]*)\\)\\s*(?::\\s*([^\\{]+))?\\{|const\\s+${method}\\s*=\\s*(?:async\\s*)?\\(([^)]*)\\)\\s*(?::\\s*([^=]+))?=>\\s*\\{)`, 'g');
|
|
199
|
+
for (const match of content.matchAll(pattern)) {
|
|
200
|
+
const openIndex = match.index + match[0].lastIndexOf('{');
|
|
201
|
+
const body = (0, extractors_1.readBalancedBlock)(content, openIndex) ?? '';
|
|
202
|
+
const params = parseBackendParams(match[1] ?? match[3] ?? '');
|
|
203
|
+
const serviceCalls = extractBackendServiceCalls(body, content, openIndex);
|
|
204
|
+
const sideEffects = extractBackendSideEffects(body, content, openIndex);
|
|
205
|
+
const errors = extractBackendErrors(body, content, openIndex);
|
|
206
|
+
const logging = extractBackendLogging(body, content, openIndex);
|
|
207
|
+
const auth = authPolicyFromDecorators([], body);
|
|
208
|
+
contracts.push({
|
|
209
|
+
framework: 'nextjs-route-handler',
|
|
210
|
+
method,
|
|
211
|
+
path: routePath,
|
|
212
|
+
fullPath: routePath,
|
|
213
|
+
handler: `${method} ${routePath}`,
|
|
214
|
+
handlerName: method,
|
|
215
|
+
line: (0, utils_1.lineNumberAt)(content, match.index),
|
|
216
|
+
decorators: [],
|
|
217
|
+
params,
|
|
218
|
+
request: requestContractFromParams(params),
|
|
219
|
+
response: {
|
|
220
|
+
type: cleanType(match[2] ?? match[4]),
|
|
221
|
+
statusCodes: statusCodesFromBody(body, method),
|
|
222
|
+
returns: extractReturnShapes(body),
|
|
223
|
+
},
|
|
224
|
+
auth,
|
|
225
|
+
validation: validationContractFromParams(params, []),
|
|
226
|
+
serviceCalls,
|
|
227
|
+
repositoryCalls: serviceCalls.filter((call) => call.kind === 'repository'),
|
|
228
|
+
sideEffects,
|
|
229
|
+
errors,
|
|
230
|
+
logging,
|
|
231
|
+
risk: backendRiskLabels({ method, auth, sideEffects, errors }),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return contracts;
|
|
236
|
+
}
|
|
237
|
+
function extractRouterEndpointContracts(relPath, content) {
|
|
238
|
+
const contracts = [];
|
|
239
|
+
const pattern = /\b(?:router|app|server|route)\.(get|post|put|patch|delete|options|head)\(\s*['"`]([^'"`]+)['"`]([\s\S]*?)\);/gi;
|
|
240
|
+
for (const match of content.matchAll(pattern)) {
|
|
241
|
+
const method = match[1].toUpperCase();
|
|
242
|
+
const routePath = (0, extractors_1.normalizeRoutePath)(match[2]);
|
|
243
|
+
const callBody = match[3] ?? '';
|
|
244
|
+
const params = parseBackendParams(callBody.match(/\(([^)]*)\)\s*=>/)?.[1] ?? 'req, res');
|
|
245
|
+
const serviceCalls = extractBackendServiceCalls(callBody, content, match.index);
|
|
246
|
+
const sideEffects = extractBackendSideEffects(callBody, content, match.index);
|
|
247
|
+
const errors = extractBackendErrors(callBody, content, match.index);
|
|
248
|
+
const logging = extractBackendLogging(callBody, content, match.index);
|
|
249
|
+
const auth = authPolicyFromMiddleware(callBody);
|
|
250
|
+
contracts.push({
|
|
251
|
+
framework: detectBackendFramework(relPath, content),
|
|
252
|
+
method,
|
|
253
|
+
path: routePath,
|
|
254
|
+
fullPath: routePath,
|
|
255
|
+
handler: `${method} ${routePath}`,
|
|
256
|
+
line: (0, utils_1.lineNumberAt)(content, match.index),
|
|
257
|
+
decorators: [],
|
|
258
|
+
params,
|
|
259
|
+
request: requestContractFromParams(params),
|
|
260
|
+
response: {
|
|
261
|
+
statusCodes: statusCodesFromBody(callBody, method),
|
|
262
|
+
returns: extractReturnShapes(callBody),
|
|
263
|
+
},
|
|
264
|
+
auth,
|
|
265
|
+
validation: validationContractFromParams(params, []),
|
|
266
|
+
serviceCalls,
|
|
267
|
+
repositoryCalls: serviceCalls.filter((call) => call.kind === 'repository'),
|
|
268
|
+
sideEffects,
|
|
269
|
+
errors,
|
|
270
|
+
logging,
|
|
271
|
+
risk: backendRiskLabels({ method, auth, sideEffects, errors }),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return contracts;
|
|
275
|
+
}
|
|
276
|
+
function extractLaravelEndpointContracts(relPath, content) {
|
|
277
|
+
const contracts = [];
|
|
278
|
+
for (const match of content.matchAll(/Route::(get|post|put|patch|delete|options|any)\(\s*['"`]([^'"`]+)['"`]([\s\S]*?)\);/gi)) {
|
|
279
|
+
const method = match[1].toLowerCase() === 'any' ? 'ANY' : match[1].toUpperCase();
|
|
280
|
+
const routePath = (0, extractors_1.normalizeRoutePath)(match[2]);
|
|
281
|
+
const callBody = match[3] ?? '';
|
|
282
|
+
const auth = authPolicyFromMiddleware(callBody);
|
|
283
|
+
contracts.push({
|
|
284
|
+
framework: 'laravel',
|
|
285
|
+
method,
|
|
286
|
+
path: routePath,
|
|
287
|
+
fullPath: routePath,
|
|
288
|
+
handler: `${method} ${routePath}`,
|
|
289
|
+
line: (0, utils_1.lineNumberAt)(content, match.index),
|
|
290
|
+
decorators: [],
|
|
291
|
+
params: [],
|
|
292
|
+
request: { pathParams: [], usesRequest: /\$request/.test(callBody), usesResponse: false },
|
|
293
|
+
response: {
|
|
294
|
+
statusCodes: statusCodesFromBody(callBody, method),
|
|
295
|
+
returns: extractReturnShapes(callBody),
|
|
296
|
+
},
|
|
297
|
+
auth,
|
|
298
|
+
validation: {
|
|
299
|
+
pathParams: [],
|
|
300
|
+
decorators: [],
|
|
301
|
+
confidence: /\bvalidate\(/.test(callBody) ? 'medium' : 'low',
|
|
302
|
+
},
|
|
303
|
+
serviceCalls: extractBackendServiceCalls(callBody, content, match.index),
|
|
304
|
+
repositoryCalls: extractBackendServiceCalls(callBody, content, match.index).filter((call) => call.kind === 'repository'),
|
|
305
|
+
sideEffects: extractBackendSideEffects(callBody, content, match.index),
|
|
306
|
+
errors: extractBackendErrors(callBody, content, match.index),
|
|
307
|
+
logging: extractBackendLogging(callBody, content, match.index),
|
|
308
|
+
risk: backendRiskLabels({ method, auth, sideEffects: [], errors: [] }),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return contracts;
|
|
312
|
+
}
|
|
313
|
+
function extractDjangoEndpointContracts(relPath, content) {
|
|
314
|
+
const contracts = [];
|
|
315
|
+
for (const match of content.matchAll(/\bpath\(\s*['"]([^'"]+)['"]\s*,\s*([A-Za-z_][\w.]*)/g)) {
|
|
316
|
+
const routePath = (0, extractors_1.normalizeRoutePath)(match[1]);
|
|
317
|
+
const auth = authPolicyFromMiddleware(content);
|
|
318
|
+
contracts.push({
|
|
319
|
+
framework: 'django',
|
|
320
|
+
method: 'ANY',
|
|
321
|
+
path: routePath,
|
|
322
|
+
fullPath: routePath,
|
|
323
|
+
handler: `ANY ${routePath}`,
|
|
324
|
+
handlerName: match[2],
|
|
325
|
+
line: (0, utils_1.lineNumberAt)(content, match.index),
|
|
326
|
+
decorators: [],
|
|
327
|
+
params: [],
|
|
328
|
+
request: { pathParams: [], usesRequest: true, usesResponse: false },
|
|
329
|
+
response: { statusCodes: [], returns: [] },
|
|
330
|
+
auth,
|
|
331
|
+
validation: { pathParams: [], decorators: [], confidence: 'low' },
|
|
332
|
+
serviceCalls: [],
|
|
333
|
+
repositoryCalls: [],
|
|
334
|
+
sideEffects: [],
|
|
335
|
+
errors: [],
|
|
336
|
+
logging: [],
|
|
337
|
+
risk: backendRiskLabels({ method: 'ANY', auth, sideEffects: [], errors: [] }),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return contracts;
|
|
341
|
+
}
|
|
342
|
+
function extractBackendServiceCalls(content, fullContent = content, offset = 0) {
|
|
343
|
+
const calls = [];
|
|
344
|
+
for (const match of content.matchAll(/\b(await\s+)?this\.([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\(/g)) {
|
|
345
|
+
if (shouldSkipBackendCallReceiver(match[2])) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
calls.push({
|
|
349
|
+
receiver: match[2],
|
|
350
|
+
method: match[3],
|
|
351
|
+
await: Boolean(match[1]),
|
|
352
|
+
line: (0, utils_1.lineNumberAt)(fullContent, offset + match.index),
|
|
353
|
+
kind: classifyCallReceiver(match[2]),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
for (const match of content.matchAll(/\b(await\s+)?([A-Za-z_$][\w$]*(?:Service|Repository|Client|Provider))\.([A-Za-z_$][\w$]*)\(/g)) {
|
|
357
|
+
if (content[match.index - 1] === '.' || shouldSkipBackendCallReceiver(match[2])) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
calls.push({
|
|
361
|
+
receiver: match[2],
|
|
362
|
+
method: match[3],
|
|
363
|
+
await: Boolean(match[1]),
|
|
364
|
+
line: (0, utils_1.lineNumberAt)(fullContent, offset + match.index),
|
|
365
|
+
kind: classifyCallReceiver(match[2]),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return uniqueServiceCalls(calls).slice(0, 80);
|
|
369
|
+
}
|
|
370
|
+
function readNestController(content) {
|
|
371
|
+
const decorator = content.match(/@Controller\(\s*(?:['"`]([^'"`]*)['"`])?/);
|
|
372
|
+
const classMatch = content.match(/(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/);
|
|
373
|
+
if (!decorator || !classMatch) {
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
name: classMatch[1],
|
|
378
|
+
basePath: decorator[1] ? (0, extractors_1.normalizeRoutePath)(decorator[1]) : '/',
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function readNestRouteDecorator(decorator) {
|
|
382
|
+
const match = decorator.match(/^@(Get|Post|Put|Patch|Delete|Options|Head)\(([^)]*)\)/);
|
|
383
|
+
const method = match?.[1]?.toUpperCase() ?? 'ANY';
|
|
384
|
+
const routeValue = match?.[2]?.match(/['"`]([^'"`]*)['"`]/)?.[1] ?? '';
|
|
385
|
+
return { method, path: (0, extractors_1.normalizeRoutePath)(routeValue) };
|
|
386
|
+
}
|
|
387
|
+
function readDecorators(block) {
|
|
388
|
+
return (0, utils_1.unique)(Array.from(block.matchAll(/@[A-Za-z_$][\w$]*(?:\([^)]*\))?/g)).map((match) => compactWhitespace(match[0])));
|
|
389
|
+
}
|
|
390
|
+
function parseBackendParams(paramsSource) {
|
|
391
|
+
const params = [];
|
|
392
|
+
for (const rawParam of splitTopLevelParams(paramsSource)) {
|
|
393
|
+
const source = backendParamSource(rawParam);
|
|
394
|
+
const decorator = rawParam.match(/@([A-Za-z_$][\w$]*)/)?.[1];
|
|
395
|
+
const decoratorName = rawParam.match(/@\w+\(\s*['"`]([^'"`]*)['"`]/)?.[1];
|
|
396
|
+
const withoutDecorators = rawParam.replace(/@[A-Za-z_$][\w$]*(?:\([^)]*\))?\s*/g, '').trim();
|
|
397
|
+
const name = withoutDecorators.match(/([A-Za-z_$][\w$]*)\??\s*(?::|=|$)/)?.[1];
|
|
398
|
+
const type = withoutDecorators.match(/:\s*([^=,]+)/)?.[1]?.trim();
|
|
399
|
+
if (!name && !decoratorName) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
params.push({
|
|
403
|
+
name: decoratorName || name || 'param',
|
|
404
|
+
source,
|
|
405
|
+
...(type ? { type: cleanType(type) } : {}),
|
|
406
|
+
required: !/\?:/.test(withoutDecorators),
|
|
407
|
+
...(decorator ? { decorator } : {}),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
return params;
|
|
411
|
+
}
|
|
412
|
+
function splitTopLevelParams(source) {
|
|
413
|
+
const params = [];
|
|
414
|
+
let start = 0;
|
|
415
|
+
let depth = 0;
|
|
416
|
+
let quote;
|
|
417
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
418
|
+
const char = source[index];
|
|
419
|
+
if (quote) {
|
|
420
|
+
if (char === quote && source[index - 1] !== '\\') {
|
|
421
|
+
quote = undefined;
|
|
422
|
+
}
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
426
|
+
quote = char;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (char === '(' || char === '[' || char === '{' || char === '<') {
|
|
430
|
+
depth += 1;
|
|
431
|
+
}
|
|
432
|
+
else if (char === ')' || char === ']' || char === '}' || char === '>') {
|
|
433
|
+
depth = Math.max(0, depth - 1);
|
|
434
|
+
}
|
|
435
|
+
else if (char === ',' && depth === 0) {
|
|
436
|
+
params.push(source.slice(start, index).trim());
|
|
437
|
+
start = index + 1;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const tail = source.slice(start).trim();
|
|
441
|
+
if (tail) {
|
|
442
|
+
params.push(tail);
|
|
443
|
+
}
|
|
444
|
+
return params.filter(Boolean);
|
|
445
|
+
}
|
|
446
|
+
function backendParamSource(rawParam) {
|
|
447
|
+
if (/@Body\b/.test(rawParam) || /\bbody\b/i.test(rawParam)) {
|
|
448
|
+
return 'body';
|
|
449
|
+
}
|
|
450
|
+
if (/@Query\b/.test(rawParam) || /\bquery\b/i.test(rawParam)) {
|
|
451
|
+
return 'query';
|
|
452
|
+
}
|
|
453
|
+
if (/@Param\b/.test(rawParam) || /\bparams?\b/i.test(rawParam)) {
|
|
454
|
+
return 'path';
|
|
455
|
+
}
|
|
456
|
+
if (/@Req\b|Request\b|\breq\b/.test(rawParam)) {
|
|
457
|
+
return 'request';
|
|
458
|
+
}
|
|
459
|
+
if (/@Res\b|Response\b|\bres\b/.test(rawParam)) {
|
|
460
|
+
return 'response';
|
|
461
|
+
}
|
|
462
|
+
if (/@CurrentUser\b|AuthenticatedUser\b|\buser\b/.test(rawParam)) {
|
|
463
|
+
return 'current-user';
|
|
464
|
+
}
|
|
465
|
+
return 'unknown';
|
|
466
|
+
}
|
|
467
|
+
function requestContractFromParams(params) {
|
|
468
|
+
return {
|
|
469
|
+
bodyType: params.find((param) => param.source === 'body')?.type,
|
|
470
|
+
queryType: params.find((param) => param.source === 'query')?.type,
|
|
471
|
+
pathParams: params.filter((param) => param.source === 'path'),
|
|
472
|
+
usesRequest: params.some((param) => param.source === 'request'),
|
|
473
|
+
usesResponse: params.some((param) => param.source === 'response'),
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function validationContractFromParams(params, decorators) {
|
|
477
|
+
const bodyType = params.find((param) => param.source === 'body')?.type;
|
|
478
|
+
const queryType = params.find((param) => param.source === 'query')?.type;
|
|
479
|
+
const validationDecorators = decorators.filter((decorator) => /^@(UsePipes|Body|Query|Param)\b/.test(decorator));
|
|
480
|
+
return {
|
|
481
|
+
...(bodyType ? { bodyType } : {}),
|
|
482
|
+
...(queryType ? { queryType } : {}),
|
|
483
|
+
pathParams: params.filter((param) => param.source === 'path'),
|
|
484
|
+
decorators: validationDecorators,
|
|
485
|
+
confidence: bodyType || queryType || validationDecorators.length ? 'high' : 'medium',
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
function authPolicyFromDecorators(decorators, body) {
|
|
489
|
+
const joined = `${decorators.join(' ')} ${body}`;
|
|
490
|
+
const guards = (0, utils_1.unique)(Array.from(joined.matchAll(/\b([A-Za-z_$][\w$]*(?:Guard|Auth|Policy|Middleware))\b/g)).map((match) => match[1]));
|
|
491
|
+
const roles = (0, utils_1.unique)(Array.from(joined.matchAll(/Roles?\(([^)]*)\)/g))
|
|
492
|
+
.flatMap((match) => Array.from(match[1].matchAll(/['"`]([^'"`]+)['"`]/g)).map((item) => item[1]))
|
|
493
|
+
.filter(Boolean));
|
|
494
|
+
const permissions = (0, utils_1.unique)(Array.from(joined.matchAll(/Permissions?\(([^)]*)\)/g))
|
|
495
|
+
.flatMap((match) => Array.from(match[1].matchAll(/['"`]([^'"`]+)['"`]/g)).map((item) => item[1]))
|
|
496
|
+
.filter(Boolean));
|
|
497
|
+
const publicEndpoint = decorators.some((decorator) => /^@(Public|AllowAnonymous)\b/.test(decorator));
|
|
498
|
+
return {
|
|
499
|
+
required: !publicEndpoint && guards.length > 0,
|
|
500
|
+
public: publicEndpoint || guards.length === 0,
|
|
501
|
+
guards,
|
|
502
|
+
roles,
|
|
503
|
+
permissions,
|
|
504
|
+
hints: authHintsFromText(joined),
|
|
505
|
+
confidence: guards.length || roles.length || permissions.length ? 'high' : 'medium',
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
function authPolicyFromMiddleware(source) {
|
|
509
|
+
const guards = (0, utils_1.unique)(Array.from(source.matchAll(/\b([A-Za-z_$][\w$]*(?:Auth|Guard|Policy|Middleware|Permission)[A-Za-z_$\w]*)\b/g)).map((match) => match[1]));
|
|
510
|
+
return {
|
|
511
|
+
required: guards.length > 0,
|
|
512
|
+
public: guards.length === 0,
|
|
513
|
+
guards,
|
|
514
|
+
roles: [],
|
|
515
|
+
permissions: [],
|
|
516
|
+
hints: authHintsFromText(source),
|
|
517
|
+
confidence: guards.length ? 'medium' : 'low',
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function authHintsFromText(source) {
|
|
521
|
+
return [
|
|
522
|
+
'auth',
|
|
523
|
+
'authenticated',
|
|
524
|
+
'authorize',
|
|
525
|
+
'permission',
|
|
526
|
+
'role',
|
|
527
|
+
'guard',
|
|
528
|
+
'session',
|
|
529
|
+
'csrf',
|
|
530
|
+
'jwt',
|
|
531
|
+
'token',
|
|
532
|
+
].filter((hint) => new RegExp(`\\b${hint}\\b`, 'i').test(source));
|
|
533
|
+
}
|
|
534
|
+
function statusCodesFromDecorators(decorators, method) {
|
|
535
|
+
const codes = decorators
|
|
536
|
+
.map((decorator) => Number(decorator.match(/@HttpCode\(\s*(\d+)/)?.[1]))
|
|
537
|
+
.filter((code) => Number.isInteger(code));
|
|
538
|
+
if (codes.length) {
|
|
539
|
+
return (0, utils_1.unique)(codes);
|
|
540
|
+
}
|
|
541
|
+
if (method === 'POST') {
|
|
542
|
+
return [201];
|
|
543
|
+
}
|
|
544
|
+
if (method === 'DELETE') {
|
|
545
|
+
return [200, 204];
|
|
546
|
+
}
|
|
547
|
+
return [200];
|
|
548
|
+
}
|
|
549
|
+
function statusCodesFromBody(body, method) {
|
|
550
|
+
const codes = Array.from(body.matchAll(/\bstatus\(\s*(\d{3})\s*\)|status\s*:\s*(\d{3})/g))
|
|
551
|
+
.map((match) => Number(match[1] ?? match[2]))
|
|
552
|
+
.filter((code) => Number.isInteger(code));
|
|
553
|
+
return codes.length ? (0, utils_1.unique)(codes) : statusCodesFromDecorators([], method);
|
|
554
|
+
}
|
|
555
|
+
function extractReturnShapes(body) {
|
|
556
|
+
const returns = new Set();
|
|
557
|
+
for (const match of body.matchAll(/\breturn\s+([^;\n]+)/g)) {
|
|
558
|
+
returns.add(compactWhitespace(match[1]).slice(0, 180));
|
|
559
|
+
}
|
|
560
|
+
return Array.from(returns).slice(0, 20);
|
|
561
|
+
}
|
|
562
|
+
function classifyCallReceiver(receiver) {
|
|
563
|
+
if (/(Repository|Repo|Store)$/i.test(receiver)) {
|
|
564
|
+
return 'repository';
|
|
565
|
+
}
|
|
566
|
+
if (/Service$/i.test(receiver)) {
|
|
567
|
+
return 'service';
|
|
568
|
+
}
|
|
569
|
+
if (/(Client|Http|Api)$/i.test(receiver)) {
|
|
570
|
+
return 'client';
|
|
571
|
+
}
|
|
572
|
+
if (/(Provider|Gateway|Manager)$/i.test(receiver)) {
|
|
573
|
+
return 'provider';
|
|
574
|
+
}
|
|
575
|
+
return /repository|repo|store/i.test(receiver)
|
|
576
|
+
? 'repository'
|
|
577
|
+
: /service/i.test(receiver)
|
|
578
|
+
? 'service'
|
|
579
|
+
: /client|http|api/i.test(receiver)
|
|
580
|
+
? 'client'
|
|
581
|
+
: 'unknown';
|
|
582
|
+
}
|
|
583
|
+
function shouldSkipBackendCallReceiver(receiver) {
|
|
584
|
+
return /^(logger|console)$/i.test(receiver);
|
|
585
|
+
}
|
|
586
|
+
function addSideEffects(effects, content, fullContent, offset, pattern, kind, confidence) {
|
|
587
|
+
for (const match of content.matchAll(pattern)) {
|
|
588
|
+
effects.push({
|
|
589
|
+
kind,
|
|
590
|
+
target: match[1] &&
|
|
591
|
+
!/^(create|update|upsert|delete|remove|save|insert|send|notify|deliver|cookie|clearCookie)$/i.test(match[1])
|
|
592
|
+
? match[1]
|
|
593
|
+
: undefined,
|
|
594
|
+
operation: match[2] ?? match[1],
|
|
595
|
+
line: (0, utils_1.lineNumberAt)(fullContent, offset + match.index),
|
|
596
|
+
confidence,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function backendRiskLabels(input) {
|
|
601
|
+
const labels = new Set();
|
|
602
|
+
if (input.method && mutatingMethods.has(input.method)) {
|
|
603
|
+
labels.add('mutation');
|
|
604
|
+
}
|
|
605
|
+
if (input.method === 'DELETE') {
|
|
606
|
+
labels.add('destructive');
|
|
607
|
+
}
|
|
608
|
+
if (input.auth?.required) {
|
|
609
|
+
labels.add('auth-protected');
|
|
610
|
+
}
|
|
611
|
+
else if (input.auth?.public) {
|
|
612
|
+
labels.add('public');
|
|
613
|
+
}
|
|
614
|
+
for (const effect of input.sideEffects) {
|
|
615
|
+
labels.add(effect.kind);
|
|
616
|
+
}
|
|
617
|
+
if (input.errors.length) {
|
|
618
|
+
labels.add('throws');
|
|
619
|
+
}
|
|
620
|
+
return Array.from(labels).sort();
|
|
621
|
+
}
|
|
622
|
+
function readClassBody(content, className) {
|
|
623
|
+
const classMatch = new RegExp(`class\\s+${className}\\b[^{]*\\{`).exec(content);
|
|
624
|
+
if (!classMatch) {
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
const openIndex = classMatch.index + classMatch[0].lastIndexOf('{');
|
|
628
|
+
const block = (0, extractors_1.readBalancedBlock)(content, openIndex);
|
|
629
|
+
if (!block) {
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
body: block.slice(1, -1),
|
|
634
|
+
offset: openIndex + 1,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function readClassMethods(classBody, offset) {
|
|
638
|
+
const methods = [];
|
|
639
|
+
const pattern = /(?:^|\n)\s*(?:public\s+|private\s+|protected\s+)?(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(([\s\S]*?)\)\s*/g;
|
|
640
|
+
let match;
|
|
641
|
+
while ((match = pattern.exec(classBody))) {
|
|
642
|
+
if (/(if|for|while|switch|catch|function)$/.test(match[1])) {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
const methodBody = findMethodBodyOpen(classBody, pattern.lastIndex);
|
|
646
|
+
if (!methodBody) {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const openIndex = methodBody.openIndex;
|
|
650
|
+
const body = (0, extractors_1.readBalancedBlock)(classBody, openIndex);
|
|
651
|
+
if (!body) {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
methods.push({
|
|
655
|
+
name: match[1],
|
|
656
|
+
params: match[2],
|
|
657
|
+
returnType: methodBody.returnType,
|
|
658
|
+
body,
|
|
659
|
+
index: offset + match.index,
|
|
660
|
+
});
|
|
661
|
+
pattern.lastIndex = openIndex + body.length;
|
|
662
|
+
}
|
|
663
|
+
return methods;
|
|
664
|
+
}
|
|
665
|
+
function findMethodBodyOpen(source, startIndex) {
|
|
666
|
+
let index = startIndex;
|
|
667
|
+
while (/\s/.test(source[index] ?? '')) {
|
|
668
|
+
index += 1;
|
|
669
|
+
}
|
|
670
|
+
if (source[index] !== ':') {
|
|
671
|
+
const openIndex = source.indexOf('{', index);
|
|
672
|
+
return openIndex >= 0 ? { openIndex } : undefined;
|
|
673
|
+
}
|
|
674
|
+
const typeStart = index + 1;
|
|
675
|
+
index = typeStart;
|
|
676
|
+
let quote;
|
|
677
|
+
let angleDepth = 0;
|
|
678
|
+
let braceDepth = 0;
|
|
679
|
+
let parenDepth = 0;
|
|
680
|
+
let bracketDepth = 0;
|
|
681
|
+
for (; index < source.length; index += 1) {
|
|
682
|
+
const char = source[index];
|
|
683
|
+
if (quote) {
|
|
684
|
+
if (char === quote && source[index - 1] !== '\\') {
|
|
685
|
+
quote = undefined;
|
|
686
|
+
}
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
690
|
+
quote = char;
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (char === '<') {
|
|
694
|
+
angleDepth += 1;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (char === '>') {
|
|
698
|
+
angleDepth = Math.max(0, angleDepth - 1);
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
if (char === '(') {
|
|
702
|
+
parenDepth += 1;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (char === ')') {
|
|
706
|
+
parenDepth = Math.max(0, parenDepth - 1);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (char === '[') {
|
|
710
|
+
bracketDepth += 1;
|
|
711
|
+
continue;
|
|
712
|
+
}
|
|
713
|
+
if (char === ']') {
|
|
714
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (char === '{') {
|
|
718
|
+
if (angleDepth === 0 && braceDepth === 0 && parenDepth === 0 && bracketDepth === 0) {
|
|
719
|
+
const previous = previousNonWhitespace(source, index - 1);
|
|
720
|
+
if (previous !== ':' && previous !== '=' && previous !== '|') {
|
|
721
|
+
return {
|
|
722
|
+
openIndex: index,
|
|
723
|
+
returnType: cleanType(source.slice(typeStart, index)),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
braceDepth += 1;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (char === '}') {
|
|
731
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
function previousNonWhitespace(source, startIndex) {
|
|
737
|
+
for (let index = startIndex; index >= 0; index -= 1) {
|
|
738
|
+
if (!/\s/.test(source[index])) {
|
|
739
|
+
return source[index];
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return undefined;
|
|
743
|
+
}
|
|
744
|
+
function readLocalHelperMethodBodies(content, className, callerBody) {
|
|
745
|
+
const classBlock = readClassBody(content, className);
|
|
746
|
+
if (!classBlock) {
|
|
747
|
+
return [];
|
|
748
|
+
}
|
|
749
|
+
const methods = readClassMethods(classBlock.body, classBlock.offset);
|
|
750
|
+
const helpers = (0, utils_1.unique)(Array.from(callerBody.matchAll(/\bthis\.([A-Za-z_$][\w$]*)\(/g)).map((match) => match[1]));
|
|
751
|
+
return methods
|
|
752
|
+
.filter((method) => helpers.includes(method.name))
|
|
753
|
+
.map((method) => ({ body: method.body, offset: method.index }));
|
|
754
|
+
}
|
|
755
|
+
function cleanType(value) {
|
|
756
|
+
return value?.replace(/\s+/g, ' ').replace(/\{$/, '').trim() || undefined;
|
|
757
|
+
}
|
|
758
|
+
function compactWhitespace(value) {
|
|
759
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
760
|
+
}
|
|
761
|
+
function joinRoutePaths(basePath, routePath) {
|
|
762
|
+
return (0, extractors_1.normalizeRoutePath)(`${(0, extractors_1.normalizeRoutePath)(basePath)}/${routePath.replace(/^\/+/, '')}`);
|
|
763
|
+
}
|
|
764
|
+
function uniqueEndpointContracts(contracts) {
|
|
765
|
+
const seen = new Set();
|
|
766
|
+
return contracts.filter((contract) => {
|
|
767
|
+
const key = `${contract.method} ${contract.fullPath ?? contract.path} ${contract.handlerName ?? ''}`;
|
|
768
|
+
if (seen.has(key)) {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
seen.add(key);
|
|
772
|
+
return true;
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
function uniqueServiceCalls(calls) {
|
|
776
|
+
const seen = new Set();
|
|
777
|
+
return calls.filter((call) => {
|
|
778
|
+
const key = `${call.receiver}.${call.method}.${call.line}`;
|
|
779
|
+
if (seen.has(key)) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
seen.add(key);
|
|
783
|
+
return true;
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
function uniqueSideEffects(effects) {
|
|
787
|
+
const seen = new Set();
|
|
788
|
+
return effects.filter((effect) => {
|
|
789
|
+
const key = `${effect.kind}.${effect.target ?? ''}.${effect.operation ?? ''}.${effect.line}`;
|
|
790
|
+
if (seen.has(key)) {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
seen.add(key);
|
|
794
|
+
return true;
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
function exceptionStatusCode(name) {
|
|
798
|
+
const statusByException = {
|
|
799
|
+
BadRequestException: 400,
|
|
800
|
+
UnauthorizedException: 401,
|
|
801
|
+
ForbiddenException: 403,
|
|
802
|
+
NotFoundException: 404,
|
|
803
|
+
ConflictException: 409,
|
|
804
|
+
GoneException: 410,
|
|
805
|
+
UnprocessableEntityException: 422,
|
|
806
|
+
TooManyRequestsException: 429,
|
|
807
|
+
InternalServerErrorException: 500,
|
|
808
|
+
};
|
|
809
|
+
return statusByException[name];
|
|
810
|
+
}
|
|
811
|
+
function backendReceiverToClassName(receiver) {
|
|
812
|
+
return (0, utils_1.toPascalCase)(receiver);
|
|
813
|
+
}
|
|
814
|
+
//# sourceMappingURL=backend.js.map
|