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.
@@ -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
+ }