carrick 0.3.126 → 0.3.128
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/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/anchors.js +5 -1
- package/sidecar/dist/src/capture/check-classify.d.ts +8 -1
- package/sidecar/dist/src/capture/check-classify.js +21 -1
- package/sidecar/dist/src/capture/check-probe.d.ts +7 -7
- package/sidecar/dist/src/capture/check-probe.js +13 -14
- package/sidecar/dist/src/capture/check.js +5 -5
- package/sidecar/dist/src/index.js +56 -4
- package/sidecar/dist/src/library-claims.d.ts +63 -3
- package/sidecar/dist/src/library-claims.js +435 -44
- package/sidecar/dist/src/retype.js +124 -8
- package/sidecar/dist/src/server-labels.d.ts +29 -0
- package/sidecar/dist/src/server-labels.js +45 -0
- package/sidecar/dist/src/stated-params.d.ts +138 -0
- package/sidecar/dist/src/stated-params.js +547 -0
- package/sidecar/dist/src/type-inferrer.d.ts +144 -8
- package/sidecar/dist/src/type-inferrer.js +635 -35
- package/sidecar/dist/src/types.d.ts +84 -3
- package/sidecar/dist/src/unwidened.d.ts +11 -2
- package/sidecar/dist/src/unwidened.js +50 -3
- package/sidecar/dist/src/validators.d.ts +228 -59
- package/sidecar/dist/src/validators.js +45 -2
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a route handler's own parameters state about the route's bodies
|
|
3
|
+
* (carrick#2674), read through the members the service's verified
|
|
4
|
+
* server-framework labels name ({@link LabelledMembers}). A member is matched
|
|
5
|
+
* by its declaration, never by its name, so a type the repo declares itself
|
|
6
|
+
* with a member of the same name states nothing here.
|
|
7
|
+
*
|
|
8
|
+
* - The request: the member a receive op's `request.body` path ends on, read
|
|
9
|
+
* at the handler's parameter. A type the source fills is the body. A body
|
|
10
|
+
* the source leaves `never`, or leaves at the library's unfilled `unknown`
|
|
11
|
+
* while it writes a type argument for the route, is "no request body",
|
|
12
|
+
* provided nothing reads it (`bodyIsUnread`).
|
|
13
|
+
* - The reply: the payload a labelled `send` member's slot takes, read at the
|
|
14
|
+
* handler's parameter, where the library declares that payload through a
|
|
15
|
+
* type parameter of the reply type. A union of success and error bodies is
|
|
16
|
+
* cut to the variants a success-status send through the parameter carries.
|
|
17
|
+
*/
|
|
18
|
+
import { Node, SyntaxKind, ts, } from 'ts-morph';
|
|
19
|
+
import { declarationKey } from './library-claims.js';
|
|
20
|
+
/** Type flags of a value that cannot hold a request body: a string, number, boolean and the like. */
|
|
21
|
+
const PLAIN_VALUE_FLAGS = ts.TypeFlags.StringLike |
|
|
22
|
+
ts.TypeFlags.NumberLike |
|
|
23
|
+
ts.TypeFlags.BigIntLike |
|
|
24
|
+
ts.TypeFlags.BooleanLike |
|
|
25
|
+
ts.TypeFlags.EnumLike |
|
|
26
|
+
ts.TypeFlags.Undefined |
|
|
27
|
+
ts.TypeFlags.Null |
|
|
28
|
+
ts.TypeFlags.Void;
|
|
29
|
+
/** The expression's type is a plain value, or a union of plain values. */
|
|
30
|
+
function holdsPlainValue(node) {
|
|
31
|
+
let type;
|
|
32
|
+
try {
|
|
33
|
+
type = node.getType();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const parts = type.isUnion() ? type.getUnionTypes() : [type];
|
|
39
|
+
return parts.every((part) => (part.getFlags() & PLAIN_VALUE_FLAGS) !== 0);
|
|
40
|
+
}
|
|
41
|
+
/** How many repo functions the request may be handed through before a read is unprovable. */
|
|
42
|
+
const REACH_DEPTH = 2;
|
|
43
|
+
export class StatedParams {
|
|
44
|
+
labels;
|
|
45
|
+
isRepoFile;
|
|
46
|
+
chainStatus;
|
|
47
|
+
constructor(labels,
|
|
48
|
+
/** True for a file the repo declares itself (not a library's or the runtime's). */
|
|
49
|
+
isRepoFile, chainStatus) {
|
|
50
|
+
this.labels = labels;
|
|
51
|
+
this.isRepoFile = isRepoFile;
|
|
52
|
+
this.chainStatus = chainStatus;
|
|
53
|
+
}
|
|
54
|
+
/** No label names a member: every reading answers nothing. */
|
|
55
|
+
get isEmpty() {
|
|
56
|
+
return this.labels.sends.size === 0 && this.labels.bodies.size === 0;
|
|
57
|
+
}
|
|
58
|
+
/** Whether some label names a reply's send member. */
|
|
59
|
+
get readsReplies() {
|
|
60
|
+
return this.labels.sends.size > 0;
|
|
61
|
+
}
|
|
62
|
+
// ===========================================================================
|
|
63
|
+
// Request
|
|
64
|
+
// ===========================================================================
|
|
65
|
+
/**
|
|
66
|
+
* The request body the handler's parameters state, the first parameter
|
|
67
|
+
* whose type has a labelled body member deciding. Undefined when no
|
|
68
|
+
* parameter has one, or the one that does states nothing decidable. A
|
|
69
|
+
* route whose `registration` hands in another function beside the handler
|
|
70
|
+
* (a middleware, which can read the body first) decides no "no body".
|
|
71
|
+
*/
|
|
72
|
+
request(handler, registration) {
|
|
73
|
+
if (this.labels.bodies.size === 0)
|
|
74
|
+
return undefined;
|
|
75
|
+
const parameters = handler.getParameters();
|
|
76
|
+
for (const parameter of parameters) {
|
|
77
|
+
const member = this.labelledMember(parameter, (key) => this.labels.bodies.has(key));
|
|
78
|
+
if (!member)
|
|
79
|
+
continue;
|
|
80
|
+
let body;
|
|
81
|
+
try {
|
|
82
|
+
body = member.getTypeAtLocation(parameter);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
if (body.isAny())
|
|
88
|
+
return undefined;
|
|
89
|
+
if (!body.isUnknown() && !body.isNever())
|
|
90
|
+
return { kind: 'body', type: body, at: parameter };
|
|
91
|
+
if (body.isUnknown() && !this.writesTypeArgument(handler, parameter))
|
|
92
|
+
return undefined;
|
|
93
|
+
if (registration && this.handsInAnotherFunction(registration, handler))
|
|
94
|
+
return undefined;
|
|
95
|
+
const declarations = new Set((member.getDeclarations() ?? []).map((d) => declarationKey(d.compilerNode)));
|
|
96
|
+
if (!this.bodyIsUnread(handler, parameters.indexOf(parameter), declarations, 0, new Map())) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
kind: 'no_body',
|
|
101
|
+
at: parameter,
|
|
102
|
+
detail: body.isNever()
|
|
103
|
+
? "the route's request type states its body as never, and nothing the handler hands the request to reads it"
|
|
104
|
+
: "the route's request type states its parts and no body, and nothing the handler hands the request to reads it",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The route hands in a function besides the handler: a callable argument
|
|
111
|
+
* (or list of them) beside the handler on any call it is passed through
|
|
112
|
+
* up to the registration, or, where the registration names the handler by
|
|
113
|
+
* reference, a second callable argument or property there. What such a
|
|
114
|
+
* function (a middleware) does with the request is not read here.
|
|
115
|
+
*/
|
|
116
|
+
handsInAnotherFunction(registration, handler) {
|
|
117
|
+
const callable = (node) => {
|
|
118
|
+
let type;
|
|
119
|
+
try {
|
|
120
|
+
type = node.getType();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
const element = type.getArrayElementType() ?? type.getTupleElements()[0];
|
|
126
|
+
return type.getCallSignatures().length > 0 || (element !== undefined && element.getCallSignatures().length > 0);
|
|
127
|
+
};
|
|
128
|
+
let node = handler;
|
|
129
|
+
let parent = node.getParent();
|
|
130
|
+
while (parent && Node.isCallExpression(parent) && parent.getArguments().includes(node)) {
|
|
131
|
+
const passed = node;
|
|
132
|
+
if (parent.getArguments().some((argument) => argument !== passed && callable(argument)))
|
|
133
|
+
return true;
|
|
134
|
+
node = parent;
|
|
135
|
+
parent = node.getParent();
|
|
136
|
+
}
|
|
137
|
+
const holdsHandler = registration === handler || handler.getAncestors().includes(registration);
|
|
138
|
+
if (holdsHandler) {
|
|
139
|
+
if (!Node.isObjectLiteralExpression(registration))
|
|
140
|
+
return false;
|
|
141
|
+
return registration
|
|
142
|
+
.getProperties()
|
|
143
|
+
.some((property) => !handler.getAncestors().includes(property) && callable(property));
|
|
144
|
+
}
|
|
145
|
+
const siblings = Node.isCallExpression(registration)
|
|
146
|
+
? registration.getArguments()
|
|
147
|
+
: Node.isObjectLiteralExpression(registration)
|
|
148
|
+
? registration.getProperties()
|
|
149
|
+
: [];
|
|
150
|
+
return siblings.filter(callable).length > 1;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* A type argument the source writes for this route: on a call the handler
|
|
154
|
+
* is passed to (the registration, or a wrapper that makes the handler),
|
|
155
|
+
* through any calls enclosing that one as an argument, or on the
|
|
156
|
+
* parameter's own annotation.
|
|
157
|
+
*/
|
|
158
|
+
writesTypeArgument(handler, parameter) {
|
|
159
|
+
const annotation = parameter.getTypeNode();
|
|
160
|
+
if (annotation && Node.isTypeReference(annotation) && annotation.getTypeArguments().length > 0) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
let node = handler;
|
|
164
|
+
let parent = node.getParent();
|
|
165
|
+
while (parent && Node.isCallExpression(parent) && parent.getArguments().includes(node)) {
|
|
166
|
+
if (parent.getTypeArguments().length > 0)
|
|
167
|
+
return true;
|
|
168
|
+
node = parent;
|
|
169
|
+
parent = node.getParent();
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Nothing reads the labelled body member off parameter `index` of
|
|
175
|
+
* `func`, nor off the request in the repo functions it is handed to, up to
|
|
176
|
+
* `REACH_DEPTH` calls deep. Every use of the request must be a member
|
|
177
|
+
* read that is not the body and is not itself called (a framework's own
|
|
178
|
+
* body reader), or a hand-off to a function the repo declares, whole and
|
|
179
|
+
* by position. Anything else (a destructured parameter, a library
|
|
180
|
+
* function, an alias, a spread) is a read this cannot rule out.
|
|
181
|
+
*/
|
|
182
|
+
bodyIsUnread(func, index, bodyKeys, depth, seen) {
|
|
183
|
+
const visited = seen.get(func) ?? new Set();
|
|
184
|
+
if (visited.has(index))
|
|
185
|
+
return true;
|
|
186
|
+
visited.add(index);
|
|
187
|
+
seen.set(func, visited);
|
|
188
|
+
const parameter = func.getParameters()[index];
|
|
189
|
+
if (!parameter)
|
|
190
|
+
return true;
|
|
191
|
+
if (parameter.isRestParameter())
|
|
192
|
+
return false;
|
|
193
|
+
const name = parameter.getNameNode();
|
|
194
|
+
if (!Node.isIdentifier(name))
|
|
195
|
+
return false;
|
|
196
|
+
const symbol = name.getSymbol();
|
|
197
|
+
const body = func.getBody();
|
|
198
|
+
if (!symbol || !body)
|
|
199
|
+
return false;
|
|
200
|
+
for (const use of body.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
201
|
+
if (use.getSymbol() !== symbol)
|
|
202
|
+
continue;
|
|
203
|
+
const parent = use.getParent();
|
|
204
|
+
if (!parent)
|
|
205
|
+
return false;
|
|
206
|
+
if (Node.isPropertyAccessExpression(parent) && parent.getExpression() === use) {
|
|
207
|
+
const declarations = parent.getNameNode().getSymbol()?.getDeclarations() ?? [];
|
|
208
|
+
if (declarations.some((d) => bodyKeys.has(declarationKey(d.compilerNode))))
|
|
209
|
+
return false;
|
|
210
|
+
if (!this.memberReadStaysPut(parent))
|
|
211
|
+
return false;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (Node.isCallExpression(parent) && parent.getArguments().includes(use)) {
|
|
215
|
+
const position = parent.getArguments().indexOf(use);
|
|
216
|
+
if (parent.getArguments().slice(0, position).some(Node.isSpreadElement))
|
|
217
|
+
return false;
|
|
218
|
+
if (depth >= REACH_DEPTH)
|
|
219
|
+
return false;
|
|
220
|
+
const callee = this.repoFunction(parent.getExpression());
|
|
221
|
+
if (!callee)
|
|
222
|
+
return false;
|
|
223
|
+
// A helper that takes the request under a type without the labelled
|
|
224
|
+
// body member could read the body under another declaration, which
|
|
225
|
+
// nothing here can match.
|
|
226
|
+
const taken = callee.getParameters()[position];
|
|
227
|
+
if (!taken || !this.labelledMember(taken, (key) => bodyKeys.has(key)))
|
|
228
|
+
return false;
|
|
229
|
+
if (!this.bodyIsUnread(callee, position, bodyKeys, depth + 1, seen))
|
|
230
|
+
return false;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* A read of a request member (`req.headers`, `req.headers.host`) whose
|
|
239
|
+
* value goes nowhere a body could be read from. Along its member chain, a
|
|
240
|
+
* method called on anything but a plain value (`req.raw.pipe(...)`, or a
|
|
241
|
+
* framework's own body reader `req.file()`) is a read, and so is a
|
|
242
|
+
* non-plain value handed to a function (`getRawBody(req.raw)`). A plain
|
|
243
|
+
* value (a header string) holds no body, whatever is done with it.
|
|
244
|
+
*/
|
|
245
|
+
memberReadStaysPut(read) {
|
|
246
|
+
let node = read;
|
|
247
|
+
for (;;) {
|
|
248
|
+
const outer = node.getParent();
|
|
249
|
+
if (!outer)
|
|
250
|
+
return true;
|
|
251
|
+
if ((Node.isCallExpression(outer) || Node.isNewExpression(outer)) && outer.getExpression() === node) {
|
|
252
|
+
const receiver = Node.isPropertyAccessExpression(node) || Node.isElementAccessExpression(node) ? node.getExpression() : undefined;
|
|
253
|
+
return receiver !== undefined && receiver !== read.getExpression() && holdsPlainValue(receiver);
|
|
254
|
+
}
|
|
255
|
+
if (Node.isCallExpression(outer) || Node.isNewExpression(outer)) {
|
|
256
|
+
return holdsPlainValue(node);
|
|
257
|
+
}
|
|
258
|
+
if ((Node.isPropertyAccessExpression(outer) || Node.isElementAccessExpression(outer)) &&
|
|
259
|
+
outer.getExpression() === node) {
|
|
260
|
+
node = outer;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (Node.isNonNullExpression(outer) || Node.isParenthesizedExpression(outer)) {
|
|
264
|
+
node = outer;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/** The function a callee names, where the repo declares it; undefined otherwise. */
|
|
271
|
+
repoFunction(callee) {
|
|
272
|
+
let symbol = callee.getSymbol();
|
|
273
|
+
if (symbol?.isAlias())
|
|
274
|
+
symbol = symbol.getAliasedSymbol() ?? symbol;
|
|
275
|
+
for (const declaration of symbol?.getDeclarations() ?? []) {
|
|
276
|
+
if (!this.isRepoFile(declaration.getSourceFile()))
|
|
277
|
+
continue;
|
|
278
|
+
if (Node.isFunctionDeclaration(declaration) ||
|
|
279
|
+
Node.isMethodDeclaration(declaration) ||
|
|
280
|
+
Node.isArrowFunction(declaration) ||
|
|
281
|
+
Node.isFunctionExpression(declaration)) {
|
|
282
|
+
return declaration.getBody() ? declaration : undefined;
|
|
283
|
+
}
|
|
284
|
+
if (Node.isVariableDeclaration(declaration) || Node.isPropertyAssignment(declaration)) {
|
|
285
|
+
const initializer = declaration.getInitializer();
|
|
286
|
+
if (initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))) {
|
|
287
|
+
return initializer;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
// ===========================================================================
|
|
294
|
+
// Reply
|
|
295
|
+
// ===========================================================================
|
|
296
|
+
/**
|
|
297
|
+
* The reply the handler's parameters state, cut to its success variants.
|
|
298
|
+
* Undefined when no parameter has a labelled send member that states a
|
|
299
|
+
* payload, when two such members disagree, when the payload is `any`,
|
|
300
|
+
* `unknown` or `never`, or when a union of bodies has no variant a
|
|
301
|
+
* success send through the parameter carries.
|
|
302
|
+
*/
|
|
303
|
+
reply(handler) {
|
|
304
|
+
if (this.labels.sends.size === 0)
|
|
305
|
+
return undefined;
|
|
306
|
+
for (const parameter of handler.getParameters()) {
|
|
307
|
+
const sends = this.sendsThrough(handler, parameter);
|
|
308
|
+
const stated = this.statedPayload(parameter, new Set(sends.flatMap((send) => send.keys)));
|
|
309
|
+
if (stated === 'none')
|
|
310
|
+
continue;
|
|
311
|
+
if (stated === 'abstain')
|
|
312
|
+
return undefined;
|
|
313
|
+
if (stated.isAny() || stated.isUnknown() || stated.isNever())
|
|
314
|
+
return undefined;
|
|
315
|
+
const variants = stated.isUnion() ? stated.getUnionTypes().filter((t) => !t.isNull() && !t.isUndefined()) : [stated];
|
|
316
|
+
const literalUnion = stated.isBoolean() || variants.every((t) => t.isLiteral() || t.isBooleanLiteral());
|
|
317
|
+
if (variants.length <= 1 || literalUnion) {
|
|
318
|
+
// An optional payload slot adds `undefined`: no body sent, not a body.
|
|
319
|
+
const defined = stated.isUnion() ? stated.getUnionTypes().filter((t) => !t.isUndefined()) : [stated];
|
|
320
|
+
return defined.length > 0 ? { variants: defined, at: parameter } : undefined;
|
|
321
|
+
}
|
|
322
|
+
const carried = this.successPayloads(sends);
|
|
323
|
+
const kept = variants.filter((variant) => carried.some((sent) => sent.isAssignableTo(variant)));
|
|
324
|
+
return kept.length > 0 ? { variants: kept, at: parameter } : undefined;
|
|
325
|
+
}
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The payload the labelled send members the handler calls through the
|
|
330
|
+
* parameter state: `none` when it calls none, `abstain` when they state
|
|
331
|
+
* different payloads, else the one payload type. A member the handler never
|
|
332
|
+
* calls (a reply object's status-code send beside its body send) says
|
|
333
|
+
* nothing about this route.
|
|
334
|
+
*/
|
|
335
|
+
statedPayload(parameter, called) {
|
|
336
|
+
let type;
|
|
337
|
+
try {
|
|
338
|
+
type = parameter.getType();
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return 'none';
|
|
342
|
+
}
|
|
343
|
+
let found = false;
|
|
344
|
+
let payload;
|
|
345
|
+
for (const property of type.getProperties()) {
|
|
346
|
+
const keys = property.getDeclarations().map((d) => declarationKey(d.compilerNode));
|
|
347
|
+
if (!keys.some((key) => called.has(key)))
|
|
348
|
+
continue;
|
|
349
|
+
const slot = this.sendSlotOf(property.getDeclarations().map((d) => d.compilerNode));
|
|
350
|
+
if (!slot)
|
|
351
|
+
continue;
|
|
352
|
+
found = true;
|
|
353
|
+
const through = property
|
|
354
|
+
.getDeclarations()
|
|
355
|
+
.map((d) => this.payloadTypeParameters(d.compilerNode, slot))
|
|
356
|
+
.find((named) => named.length > 0);
|
|
357
|
+
if (!through)
|
|
358
|
+
continue;
|
|
359
|
+
const checker = parameter.getProject().getTypeChecker().compilerObject;
|
|
360
|
+
const defaults = new Set(through.flatMap((named) => (named.default ? [checker.getTypeFromTypeNode(named.default)] : [])));
|
|
361
|
+
let member;
|
|
362
|
+
try {
|
|
363
|
+
member = property.getTypeAtLocation(parameter);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
for (const signature of member.getCallSignatures()) {
|
|
369
|
+
// A payload the member's own type parameter types is whatever each
|
|
370
|
+
// call passes: the parameter states nothing.
|
|
371
|
+
if (signature.getTypeParameters().length > 0)
|
|
372
|
+
continue;
|
|
373
|
+
const at = this.payloadAt(signature.getParameters(), slot, parameter);
|
|
374
|
+
if (!at)
|
|
375
|
+
continue;
|
|
376
|
+
// The type parameter at its declared default: the route filled
|
|
377
|
+
// nothing there (a status-code send left at `number`).
|
|
378
|
+
if (defaults.has(at.compilerType))
|
|
379
|
+
continue;
|
|
380
|
+
if (payload === undefined)
|
|
381
|
+
payload = at;
|
|
382
|
+
else if (payload.compilerType !== at.compilerType)
|
|
383
|
+
return 'abstain';
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!found)
|
|
387
|
+
return 'none';
|
|
388
|
+
return payload ?? 'none';
|
|
389
|
+
}
|
|
390
|
+
/** The payload slot a labelled send member takes, by any of its declarations. */
|
|
391
|
+
sendSlotOf(declarations) {
|
|
392
|
+
for (const declaration of declarations) {
|
|
393
|
+
const slot = this.labels.sends.get(declarationKey(declaration));
|
|
394
|
+
if (slot)
|
|
395
|
+
return slot;
|
|
396
|
+
}
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* The type parameters of the type that declares the member that its
|
|
401
|
+
* payload is declared through: a slot each route fills. None for a payload
|
|
402
|
+
* declared with a fixed type (a file path), which says nothing about any
|
|
403
|
+
* one route.
|
|
404
|
+
*/
|
|
405
|
+
payloadTypeParameters(declaration, slot) {
|
|
406
|
+
const owner = declaration.parent;
|
|
407
|
+
const ownParameters = new Set(owner && (ts.isInterfaceDeclaration(owner) || ts.isClassDeclaration(owner) || ts.isClassExpression(owner))
|
|
408
|
+
? (owner.typeParameters ?? [])
|
|
409
|
+
: []);
|
|
410
|
+
if (ownParameters.size === 0)
|
|
411
|
+
return [];
|
|
412
|
+
let typeNode;
|
|
413
|
+
if (ts.isMethodSignature(declaration) || ts.isMethodDeclaration(declaration)) {
|
|
414
|
+
typeNode = declaration.parameters[slot.arg]?.type;
|
|
415
|
+
}
|
|
416
|
+
else if (ts.isPropertySignature(declaration) || ts.isPropertyDeclaration(declaration)) {
|
|
417
|
+
typeNode = declaration.type;
|
|
418
|
+
}
|
|
419
|
+
if (!typeNode)
|
|
420
|
+
return [];
|
|
421
|
+
const mentioned = new Set();
|
|
422
|
+
const visit = (node) => {
|
|
423
|
+
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName)) {
|
|
424
|
+
const name = node.typeName.text;
|
|
425
|
+
for (const parameter of ownParameters) {
|
|
426
|
+
if (ts.isTypeParameterDeclaration(parameter) && parameter.name.text === name)
|
|
427
|
+
mentioned.add(parameter);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
ts.forEachChild(node, visit);
|
|
431
|
+
};
|
|
432
|
+
visit(typeNode);
|
|
433
|
+
return [...mentioned];
|
|
434
|
+
}
|
|
435
|
+
/** The type the call signature takes at the payload slot, read at `at`. */
|
|
436
|
+
payloadAt(parameters, slot, at) {
|
|
437
|
+
const symbol = parameters[slot.arg];
|
|
438
|
+
if (!symbol)
|
|
439
|
+
return undefined;
|
|
440
|
+
const declaration = symbol.getDeclarations()[0];
|
|
441
|
+
let type = symbol.getTypeAtLocation(at);
|
|
442
|
+
if (declaration && Node.isParameterDeclaration(declaration) && declaration.isRestParameter()) {
|
|
443
|
+
if (!type.isTuple())
|
|
444
|
+
return undefined;
|
|
445
|
+
const first = type.getTupleElements()[0];
|
|
446
|
+
if (!first)
|
|
447
|
+
return undefined;
|
|
448
|
+
type = first;
|
|
449
|
+
}
|
|
450
|
+
if (slot.key !== undefined) {
|
|
451
|
+
const property = type.getProperty(slot.key);
|
|
452
|
+
if (!property)
|
|
453
|
+
return undefined;
|
|
454
|
+
type = property.getTypeAtLocation(at);
|
|
455
|
+
}
|
|
456
|
+
const optional = declaration !== undefined && Node.isParameterDeclaration(declaration) && declaration.isOptional();
|
|
457
|
+
if (optional && type.isUnion()) {
|
|
458
|
+
const defined = type.getUnionTypes().filter((t) => !t.isUndefined());
|
|
459
|
+
if (defined.length === 1)
|
|
460
|
+
return defined[0];
|
|
461
|
+
}
|
|
462
|
+
return type;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* The calls of a labelled send member the handler makes on `parameter`, or
|
|
466
|
+
* on a call chain rooted at it (`res.status(201).json(body)`), each with
|
|
467
|
+
* its payload slot and the keys of the member's declarations.
|
|
468
|
+
*/
|
|
469
|
+
sendsThrough(handler, parameter) {
|
|
470
|
+
const name = parameter.getNameNode();
|
|
471
|
+
const symbol = Node.isIdentifier(name) ? name.getSymbol() : undefined;
|
|
472
|
+
const body = handler.getBody();
|
|
473
|
+
if (!symbol || !body)
|
|
474
|
+
return [];
|
|
475
|
+
const sends = [];
|
|
476
|
+
for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
477
|
+
const callee = call.getExpression();
|
|
478
|
+
if (!Node.isPropertyAccessExpression(callee))
|
|
479
|
+
continue;
|
|
480
|
+
const declarations = (callee.getNameNode().getSymbol()?.getDeclarations() ?? []).map((d) => d.compilerNode);
|
|
481
|
+
const slot = this.sendSlotOf(declarations);
|
|
482
|
+
if (!slot)
|
|
483
|
+
continue;
|
|
484
|
+
if (this.rootSymbol(callee.getExpression()) !== symbol)
|
|
485
|
+
continue;
|
|
486
|
+
sends.push({ call, slot, keys: declarations.map(declarationKey) });
|
|
487
|
+
}
|
|
488
|
+
return sends;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* The payloads the sends carry whose chain states a success status or
|
|
492
|
+
* none. A value typed `any` or `unknown` is assignable to every variant, so
|
|
493
|
+
* it tells no success body from an error body and is left out.
|
|
494
|
+
*/
|
|
495
|
+
successPayloads(sends) {
|
|
496
|
+
const sent = [];
|
|
497
|
+
for (const { call, slot } of sends) {
|
|
498
|
+
const status = this.chainStatus(call);
|
|
499
|
+
if (status !== undefined && status !== 'success')
|
|
500
|
+
continue;
|
|
501
|
+
let argument = call.getArguments()[slot.arg];
|
|
502
|
+
if (!argument || Node.isSpreadElement(argument))
|
|
503
|
+
continue;
|
|
504
|
+
if (slot.key !== undefined) {
|
|
505
|
+
if (!Node.isObjectLiteralExpression(argument))
|
|
506
|
+
continue;
|
|
507
|
+
const property = argument.getProperty(slot.key);
|
|
508
|
+
if (!property || !Node.isPropertyAssignment(property))
|
|
509
|
+
continue;
|
|
510
|
+
const initializer = property.getInitializer();
|
|
511
|
+
if (!initializer)
|
|
512
|
+
continue;
|
|
513
|
+
argument = initializer;
|
|
514
|
+
}
|
|
515
|
+
const type = argument.getType();
|
|
516
|
+
if (type.isAny() || type.isUnknown())
|
|
517
|
+
continue;
|
|
518
|
+
sent.push(type);
|
|
519
|
+
}
|
|
520
|
+
return sent;
|
|
521
|
+
}
|
|
522
|
+
/** The symbol at the root of a member or call chain (`res.status(201)` is `res`). */
|
|
523
|
+
rootSymbol(expression) {
|
|
524
|
+
let node = expression;
|
|
525
|
+
for (;;) {
|
|
526
|
+
if (Node.isPropertyAccessExpression(node) || Node.isCallExpression(node) || Node.isParenthesizedExpression(node) || Node.isNonNullExpression(node)) {
|
|
527
|
+
node = node.getExpression();
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
return Node.isIdentifier(node) ? node.getSymbol() : undefined;
|
|
533
|
+
}
|
|
534
|
+
/** The member of the parameter's type whose declaration a label names. */
|
|
535
|
+
labelledMember(parameter, labelled) {
|
|
536
|
+
let type;
|
|
537
|
+
try {
|
|
538
|
+
type = parameter.getType();
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
return type
|
|
544
|
+
.getProperties()
|
|
545
|
+
.find((property) => property.getDeclarations().some((d) => labelled(declarationKey(d.compilerNode))));
|
|
546
|
+
}
|
|
547
|
+
}
|