carrick 0.3.127 → 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 +43 -4
- package/sidecar/dist/src/library-claims.d.ts +63 -3
- package/sidecar/dist/src/library-claims.js +380 -39
- package/sidecar/dist/src/retype.js +36 -0
- 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 +89 -1
- package/sidecar/dist/src/type-inferrer.js +429 -17
- package/sidecar/dist/src/types.d.ts +76 -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
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { Project, SourceFile, type CallExpression } from 'ts-morph';
|
|
21
21
|
import type { InferRequestItem, InferResult, ModelSymbol, ExtractionConfig } from './types.js';
|
|
22
|
+
import type { LabelledMembers } from './library-claims.js';
|
|
22
23
|
/**
|
|
23
24
|
* Strongly-discriminating member names of HTTP transport machinery — the
|
|
24
25
|
* fetch/DOM `Response` & `Request`, a Node `http.ServerResponse`, a framework
|
|
@@ -123,9 +124,15 @@ export declare class TypeInferrer {
|
|
|
123
124
|
* @param onRequestDone - Called once per request, as the batch is done with it
|
|
124
125
|
* @returns InferResult with inferred types or errors
|
|
125
126
|
*/
|
|
126
|
-
infer(requests: InferRequestItem[], extractionConfig?: ExtractionConfig, onRequestDone?: InferRequestDone, modelSymbols?: ModelSymbol[]): InferResult;
|
|
127
|
+
infer(requests: InferRequestItem[], extractionConfig?: ExtractionConfig, onRequestDone?: InferRequestDone, modelSymbols?: ModelSymbol[], labels?: LabelledMembers): InferResult;
|
|
127
128
|
/** The types the model named, by the alias of the row each is for (carrick#2111). */
|
|
128
129
|
private modelSymbols;
|
|
130
|
+
/**
|
|
131
|
+
* carrick#2674: what route handlers' parameters state, through the
|
|
132
|
+
* members the batch's verified server-framework labels name. Empty when
|
|
133
|
+
* the batch carries no labels, and then it reads nothing.
|
|
134
|
+
*/
|
|
135
|
+
private statedParams;
|
|
129
136
|
/**
|
|
130
137
|
* carrick#2111: where the model named a type for the response this request
|
|
131
138
|
* reads, record on `inferred` how that type relates to the compiler's type
|
|
@@ -161,6 +168,10 @@ export declare class TypeInferrer {
|
|
|
161
168
|
* really returns beside the published one (`unwidened_type_string`). See
|
|
162
169
|
* `unwidened.ts` for what is marked and why the reading is sound.
|
|
163
170
|
*
|
|
171
|
+
* carrick#2011: a request body (`read` is `'request_body'`) is read the same
|
|
172
|
+
* way, the call that sends it in place of the handler, except that a literal
|
|
173
|
+
* handed to any call other than the send is never marked.
|
|
174
|
+
*
|
|
164
175
|
* One rewrite for the whole batch: every file on any request's path is
|
|
165
176
|
* rewritten once, type-checked before and after, the requests re-inferred,
|
|
166
177
|
* and every file restored before returning, so the project other requests
|
|
@@ -643,6 +654,72 @@ export declare class TypeInferrer {
|
|
|
643
654
|
private resultCarrierArguments;
|
|
644
655
|
/** The member names every branch holds, each with one and the same type. */
|
|
645
656
|
private membersCommonToEveryBranch;
|
|
657
|
+
/**
|
|
658
|
+
* What the source narrows a call's value to, where the type stated at the
|
|
659
|
+
* walk's terminal is a placeholder that leaves a position open
|
|
660
|
+
* (carrick#2714, `as { data: Item[] } | { error: unknown }`): the one type
|
|
661
|
+
* stated for the value later in the function, on its success path
|
|
662
|
+
* (`return json as { data: Item[] }`). A closed statement at the terminal
|
|
663
|
+
* keeps the answer it had.
|
|
664
|
+
*/
|
|
665
|
+
private narrowedBodyStatement;
|
|
666
|
+
/**
|
|
667
|
+
* The rule that verifies a call's result as a request library's response
|
|
668
|
+
* object, or undefined (carrick#2714). The result is that object when its
|
|
669
|
+
* awaited type is one a rule recognises. It is that object too when the
|
|
670
|
+
* call states no type arguments and its declared result, with its free
|
|
671
|
+
* type parameters at their defaults (`freeResultAtDefaults`), is a type a
|
|
672
|
+
* rule recognises (`get<T, R = Response<T>>(): Promise<R>`).
|
|
673
|
+
* The compiler then takes the parameter from where the value goes
|
|
674
|
+
* (`return client.post(…)` in a function declared `Promise<void>` types the
|
|
675
|
+
* call `Promise<void>`), and that is the caller's claim about the object,
|
|
676
|
+
* not what the call returns.
|
|
677
|
+
*/
|
|
678
|
+
private transportRuleOfCall;
|
|
679
|
+
/**
|
|
680
|
+
* The first rule that matches `type` as a wrapper whose body is a member
|
|
681
|
+
* (`payloadPropertyPath`) the type has, whether or not it reads a payload
|
|
682
|
+
* out of it. A
|
|
683
|
+
* wrapper with no such member (the platform's `Response`) is read by its
|
|
684
|
+
* body reads (`res.json()`), which the def-use walk follows.
|
|
685
|
+
*/
|
|
686
|
+
private ruleRecognising;
|
|
687
|
+
/**
|
|
688
|
+
* What a call's declared result is when its free type parameters take
|
|
689
|
+
* their defaults, or undefined. A parameter is free when the declaration's
|
|
690
|
+
* result mentions it and none of its parameters do, so only a type
|
|
691
|
+
* argument, the default or the call's return position can fix it. The
|
|
692
|
+
* result is read through `Promise`, a free parameter (its default), and a
|
|
693
|
+
* conditional alias whose check type is a free parameter tested against
|
|
694
|
+
* exactly that parameter's default (`R extends Default ? Response<T> : R`:
|
|
695
|
+
* the true branch). Anything else is the type as written.
|
|
696
|
+
*/
|
|
697
|
+
private freeResultAtDefaults;
|
|
698
|
+
/**
|
|
699
|
+
* The call states the body itself: the rules read a payload out of its
|
|
700
|
+
* result (`client.get<Widget>(url)`). An untyped payload is no payload to
|
|
701
|
+
* the rules (they verify the object and read nothing), and a result its
|
|
702
|
+
* return position chose is no response object to them.
|
|
703
|
+
*/
|
|
704
|
+
private callStatesTypedPayload;
|
|
705
|
+
/**
|
|
706
|
+
* The body of a call that returns a response object (carrick#2714): the
|
|
707
|
+
* one type the source states, in the function the call is made in, for the
|
|
708
|
+
* reads of the object's payload member (`res.data as Widget`, `const w:
|
|
709
|
+
* Widget = res.data`, `return res.data` from a function that declares its
|
|
710
|
+
* return type), through a binding or in place. Where no read states one,
|
|
711
|
+
* or two state different types, or the object is handed on whole, the
|
|
712
|
+
* site states no response contract, and the abstain is decided so the
|
|
713
|
+
* capture does not re-read the call and publish the object.
|
|
714
|
+
*/
|
|
715
|
+
private transportCallBody;
|
|
716
|
+
/**
|
|
717
|
+
* The reads of a response object's payload member (`path`, e.g. `data`)
|
|
718
|
+
* that take it off the call's value: in place (`(await call).data`),
|
|
719
|
+
* through the name the value is bound to (`res.data`), or as a name
|
|
720
|
+
* destructured out of it (`const { data } = await call`).
|
|
721
|
+
*/
|
|
722
|
+
private payloadMemberReads;
|
|
646
723
|
/**
|
|
647
724
|
* The decided abstain of a call whose result carries transport the
|
|
648
725
|
* service's wrapper rules verify and read no payload out of (carrick#1841,
|
|
@@ -2090,6 +2167,17 @@ export declare class TypeInferrer {
|
|
|
2090
2167
|
* own locator authoritative when a route declares nothing.
|
|
2091
2168
|
*/
|
|
2092
2169
|
private declaredRequestContract;
|
|
2170
|
+
/** The stated-parameter reader over `labels`, judging files and statuses as this inferrer does. */
|
|
2171
|
+
private statedParamsFor;
|
|
2172
|
+
/**
|
|
2173
|
+
* carrick#2674: where today's reading of a route's response is empty
|
|
2174
|
+
* (nothing, `void`, or an `unknown` the inferrer did not decide), the reply
|
|
2175
|
+
* its handler's parameter states, cut to its success variants. A typed
|
|
2176
|
+
* reading, or a decided abstain, stands as it is.
|
|
2177
|
+
*/
|
|
2178
|
+
private withStatedReply;
|
|
2179
|
+
/** Nothing read: `void`, or an `unknown` with no decided reason at its root. */
|
|
2180
|
+
private isEmptyResponseReading;
|
|
2093
2181
|
/**
|
|
2094
2182
|
* Anchor (a): the request contract declared on the handler's own signature.
|
|
2095
2183
|
*
|