carrick 0.3.130 → 0.3.132
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/dist/init/hosted.js +3 -1
- package/dist/init/hosted.js.map +1 -1
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/absent-body.d.ts +41 -0
- package/sidecar/dist/src/absent-body.js +224 -0
- package/sidecar/dist/src/capture/api.d.ts +20 -0
- package/sidecar/dist/src/capture/check-classify.d.ts +5 -1
- package/sidecar/dist/src/capture/check-classify.js +20 -1
- package/sidecar/dist/src/capture/check-probe.d.ts +5 -1
- package/sidecar/dist/src/capture/check-probe.js +38 -4
- package/sidecar/dist/src/capture/check-scrub.js +2 -0
- package/sidecar/dist/src/capture/check.js +8 -1
- package/sidecar/dist/src/capture/self-check.js +16 -6
- package/sidecar/dist/src/retype.js +42 -0
- package/sidecar/dist/src/stated-params.d.ts +1 -1
- package/sidecar/dist/src/type-inferrer.d.ts +19 -0
- package/sidecar/dist/src/type-inferrer.js +79 -2
- package/sidecar/dist/src/types.d.ts +8 -0
- package/sidecar/dist/src/validators.d.ts +598 -578
- package/sidecar/dist/src/validators.js +2 -0
|
@@ -195,6 +195,12 @@ export class Retyper {
|
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
197
|
if (declaresTypeParameters(call)) {
|
|
198
|
+
const inferred = inferredTypeParameterOf(call);
|
|
199
|
+
if (inferred) {
|
|
200
|
+
return (`the callee declares several type parameters and '${inferred}' is ` +
|
|
201
|
+
`inferred from the arguments, so writing one type argument would turn ` +
|
|
202
|
+
`its inference off`);
|
|
203
|
+
}
|
|
198
204
|
const open = call.getFirstChildByKind(SyntaxKind.OpenParenToken);
|
|
199
205
|
if (!open)
|
|
200
206
|
return 'the consumer call has no argument list to retype';
|
|
@@ -948,6 +954,42 @@ function bodyReadEdit(read) {
|
|
|
948
954
|
{ start: end, end, text: ` as Promise<${stated}>)`, statedAt: ' as Promise<'.length },
|
|
949
955
|
];
|
|
950
956
|
}
|
|
957
|
+
/**
|
|
958
|
+
* The name of a type parameter after the first that the call's arguments
|
|
959
|
+
* infer and whose default would change the call if inference were off, or
|
|
960
|
+
* `undefined`.
|
|
961
|
+
*
|
|
962
|
+
* Writing one type argument makes every other parameter take its default
|
|
963
|
+
* instead of its inferred type. A parameter the argument list mentions
|
|
964
|
+
* (`TVariables` in `options: Options<TData, TError, TVariables>`) and whose
|
|
965
|
+
* default is not `any` or `unknown` then types the argument against the
|
|
966
|
+
* default (`void`), and the call reports a diagnostic about the written
|
|
967
|
+
* argument, not about anything the consumer reads. A parameter nothing in the
|
|
968
|
+
* argument list mentions, or one defaulting to `any`, is unaffected.
|
|
969
|
+
*/
|
|
970
|
+
function inferredTypeParameterOf(call) {
|
|
971
|
+
const declaration = resolvedDeclaration(call);
|
|
972
|
+
const typeParameters = declaration?.typeParameters;
|
|
973
|
+
if (!declaration || !typeParameters || typeParameters.length < 2)
|
|
974
|
+
return undefined;
|
|
975
|
+
for (const parameter of typeParameters.slice(1)) {
|
|
976
|
+
const fallback = parameter.default;
|
|
977
|
+
if (fallback &&
|
|
978
|
+
(fallback.kind === ts.SyntaxKind.AnyKeyword || fallback.kind === ts.SyntaxKind.UnknownKeyword)) {
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
const name = parameter.name.text;
|
|
982
|
+
if (declaration.parameters.some((p) => p.type && mentionsTypeName(p.type, name)))
|
|
983
|
+
return name;
|
|
984
|
+
}
|
|
985
|
+
return undefined;
|
|
986
|
+
}
|
|
987
|
+
function mentionsTypeName(node, name) {
|
|
988
|
+
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && node.typeName.text === name) {
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
return ts.forEachChild(node, (child) => (mentionsTypeName(child, name) ? true : undefined)) ?? false;
|
|
992
|
+
}
|
|
951
993
|
function declaresTypeParameters(call) {
|
|
952
994
|
return (resolvedDeclaration(call)?.typeParameters?.length ?? 0) > 0;
|
|
953
995
|
}
|
|
@@ -63,7 +63,7 @@ export declare class StatedParams {
|
|
|
63
63
|
* reference, a second callable argument or property there. What such a
|
|
64
64
|
* function (a middleware) does with the request is not read here.
|
|
65
65
|
*/
|
|
66
|
-
|
|
66
|
+
handsInAnotherFunction(registration: Node, handler: HandlerFunction): boolean;
|
|
67
67
|
/**
|
|
68
68
|
* A type argument the source writes for this route: on a call the handler
|
|
69
69
|
* is passed to (the registration, or a wrapper that makes the handler),
|
|
@@ -234,6 +234,18 @@ export declare class TypeInferrer {
|
|
|
234
234
|
* truncation.
|
|
235
235
|
*/
|
|
236
236
|
private inferFunctionParam;
|
|
237
|
+
/**
|
|
238
|
+
* carrick#2814: mark a route's request body as one its handler tolerates
|
|
239
|
+
* going unsent, when the handler is in reach and every read of the body
|
|
240
|
+
* survives `undefined` (`absent-body.ts`). The check then lets a caller that
|
|
241
|
+
* may send nothing call a route whose body type has no required member.
|
|
242
|
+
*
|
|
243
|
+
* No mark where the handler cannot be found, where a validator the
|
|
244
|
+
* registration declares reads the body first (a schema, or another function
|
|
245
|
+
* handed in beside the handler), or where the handler reads no body it can
|
|
246
|
+
* see: the check keeps the reading it had.
|
|
247
|
+
*/
|
|
248
|
+
private withAbsentBodyMark;
|
|
237
249
|
/**
|
|
238
250
|
* Resolve a `function_param` locator against a function's parameter list.
|
|
239
251
|
* Three shapes, tried in order:
|
|
@@ -2530,6 +2542,13 @@ export declare class TypeInferrer {
|
|
|
2530
2542
|
* then substring match (preferring containing matches).
|
|
2531
2543
|
*/
|
|
2532
2544
|
private matchByText;
|
|
2545
|
+
/**
|
|
2546
|
+
* The text a locator opens with before an elision, or `undefined` when the
|
|
2547
|
+
* locator has none. An elision is `...` followed only by closing brackets
|
|
2548
|
+
* (`{ a: x, ... }`); `...rest` is a spread and source text. An opening too
|
|
2549
|
+
* short to tell nodes apart is judged by the caller.
|
|
2550
|
+
*/
|
|
2551
|
+
private elidedLocatorOpening;
|
|
2533
2552
|
/**
|
|
2534
2553
|
* What a locator's text names, read off its own parse (carrick#2056):
|
|
2535
2554
|
*
|
|
@@ -32,6 +32,7 @@ import { attributedDeclaration, installedPackageOf } from './declaration-attribu
|
|
|
32
32
|
import { endLineOf, startLineOf } from './line-index.js';
|
|
33
33
|
import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
|
|
34
34
|
import { StatedParams } from './stated-params.js';
|
|
35
|
+
import { bindingToleratesAbsence, handlerToleratesAbsentBody } from './absent-body.js';
|
|
35
36
|
import { classifyStatusCodes, constituents, httpStatus, statesNoBody, statesNoShape, statusKeyCode, statusTableOf, successEntries, } from './capture/index.js';
|
|
36
37
|
import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
|
|
37
38
|
import { expandTypeStructural, jsonWireType, } from './type-structural-expander.js';
|
|
@@ -883,7 +884,7 @@ export class TypeInferrer {
|
|
|
883
884
|
case 'expression':
|
|
884
885
|
return this.inferExpression(sourceFile, request, extractionConfig);
|
|
885
886
|
case 'request_body':
|
|
886
|
-
return this.inferRequestBody(sourceFile, request, extractionConfig);
|
|
887
|
+
return this.withAbsentBodyMark(sourceFile, request, this.inferRequestBody(sourceFile, request, extractionConfig));
|
|
887
888
|
case 'signature_return':
|
|
888
889
|
return this.inferSignatureReturn(sourceFile, request);
|
|
889
890
|
case 'function_param':
|
|
@@ -1168,7 +1169,51 @@ export class TypeInferrer {
|
|
|
1168
1169
|
// scanner re-aims the explicit bundle at the root symbol instead of
|
|
1169
1170
|
// pasting this string, so the alias's definition stays self-contained.
|
|
1170
1171
|
const anchor = this.unwrapArrayLevels(this.unwrapPromiseType(paramType));
|
|
1171
|
-
|
|
1172
|
+
const inferred = this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(target.node), undefined, this.primaryTypeSymbol(anchor.element), anchor.depth, this.primaryTypeSymbolSource(anchor.element));
|
|
1173
|
+
// carrick#2814: a subscriber that defaults its payload, or reads it only
|
|
1174
|
+
// in ways `undefined` survives, still works when none is sent.
|
|
1175
|
+
const binding = Node.isBindingElement(target.node) ? target.node : target.param;
|
|
1176
|
+
if (bindingToleratesAbsence(binding)) {
|
|
1177
|
+
inferred.absent_body_tolerated = true;
|
|
1178
|
+
}
|
|
1179
|
+
return inferred;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* carrick#2814: mark a route's request body as one its handler tolerates
|
|
1183
|
+
* going unsent, when the handler is in reach and every read of the body
|
|
1184
|
+
* survives `undefined` (`absent-body.ts`). The check then lets a caller that
|
|
1185
|
+
* may send nothing call a route whose body type has no required member.
|
|
1186
|
+
*
|
|
1187
|
+
* No mark where the handler cannot be found, where a validator the
|
|
1188
|
+
* registration declares reads the body first (a schema, or another function
|
|
1189
|
+
* handed in beside the handler), or where the handler reads no body it can
|
|
1190
|
+
* see: the check keeps the reading it had.
|
|
1191
|
+
*/
|
|
1192
|
+
withAbsentBodyMark(sourceFile, request, inferred) {
|
|
1193
|
+
if (!inferred)
|
|
1194
|
+
return inferred;
|
|
1195
|
+
const atLine = this.registrationAtLine(sourceFile, request.line_number);
|
|
1196
|
+
let registration = atLine?.registration;
|
|
1197
|
+
let handler = atLine?.handler;
|
|
1198
|
+
if (!handler) {
|
|
1199
|
+
const located = this.resolveTargetNode(sourceFile, request);
|
|
1200
|
+
handler = located ? this.registrationHandlerAt(located) : undefined;
|
|
1201
|
+
registration = handler && located ? this.unwrapExpressionNode(located) : undefined;
|
|
1202
|
+
}
|
|
1203
|
+
handler ??= this.exportedFunctionOpeningOn(sourceFile, request.line_number);
|
|
1204
|
+
if (!handler)
|
|
1205
|
+
return inferred;
|
|
1206
|
+
if (registration) {
|
|
1207
|
+
if (this.routeSchemaContract(registration, 'body') ?? this.validatedBodyContract(registration)) {
|
|
1208
|
+
return inferred;
|
|
1209
|
+
}
|
|
1210
|
+
if (this.statedParams.handsInAnotherFunction(registration, handler))
|
|
1211
|
+
return inferred;
|
|
1212
|
+
}
|
|
1213
|
+
if (handlerToleratesAbsentBody(handler)) {
|
|
1214
|
+
inferred.absent_body_tolerated = true;
|
|
1215
|
+
}
|
|
1216
|
+
return inferred;
|
|
1172
1217
|
}
|
|
1173
1218
|
/**
|
|
1174
1219
|
* Resolve a `function_param` locator against a function's parameter list.
|
|
@@ -9309,6 +9354,26 @@ export class TypeInferrer {
|
|
|
9309
9354
|
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(normalizedTarget)) {
|
|
9310
9355
|
return undefined;
|
|
9311
9356
|
}
|
|
9357
|
+
// A locator that elides members (`body: { a: x, ... }`) stands for the
|
|
9358
|
+
// whole node it opens: the text before the elision is where the source
|
|
9359
|
+
// opens. Never a node the text merely contains, which is one member of the
|
|
9360
|
+
// payload it names.
|
|
9361
|
+
// An opening shorter than this (`{`) names no node in particular.
|
|
9362
|
+
const MIN_ELIDED_OPENING_LEN = 8;
|
|
9363
|
+
const elidedOpening = this.elidedLocatorOpening(normalizedTarget);
|
|
9364
|
+
if (elidedOpening !== undefined) {
|
|
9365
|
+
if (elidedOpening.length < MIN_ELIDED_OPENING_LEN)
|
|
9366
|
+
return undefined;
|
|
9367
|
+
const opened = candidates.filter((c) => c.text.startsWith(elidedOpening));
|
|
9368
|
+
if (opened.length === 0)
|
|
9369
|
+
return undefined;
|
|
9370
|
+
if (lineNumber !== undefined) {
|
|
9371
|
+
const distance = (c) => Math.abs(startLineOf(c.node) - lineNumber);
|
|
9372
|
+
const nearest = Math.min(...opened.map(distance));
|
|
9373
|
+
return this.pickBestMatch(opened.filter((c) => distance(c) === nearest).map((c) => c.node), lineNumber);
|
|
9374
|
+
}
|
|
9375
|
+
return this.pickBestMatch(opened.map((c) => c.node), lineNumber);
|
|
9376
|
+
}
|
|
9312
9377
|
// Fall back to substring match
|
|
9313
9378
|
// For the reverse direction (target contains node text), require a minimum node text
|
|
9314
9379
|
// length to avoid matching tiny identifiers like "res" or "body" too broadly.
|
|
@@ -9375,6 +9440,18 @@ export class TypeInferrer {
|
|
|
9375
9440
|
}
|
|
9376
9441
|
return bound.size > 0 ? this.pickBestMatch([...bound], lineNumber) : undefined;
|
|
9377
9442
|
}
|
|
9443
|
+
/**
|
|
9444
|
+
* The text a locator opens with before an elision, or `undefined` when the
|
|
9445
|
+
* locator has none. An elision is `...` followed only by closing brackets
|
|
9446
|
+
* (`{ a: x, ... }`); `...rest` is a spread and source text. An opening too
|
|
9447
|
+
* short to tell nodes apart is judged by the caller.
|
|
9448
|
+
*/
|
|
9449
|
+
elidedLocatorOpening(normalizedTarget) {
|
|
9450
|
+
const match = /^(.*?)[\s,]*\.\.\.[\s,)}\]]*$/s.exec(normalizedTarget);
|
|
9451
|
+
if (!match)
|
|
9452
|
+
return undefined;
|
|
9453
|
+
return match[1];
|
|
9454
|
+
}
|
|
9378
9455
|
/**
|
|
9379
9456
|
* What a locator's text names, read off its own parse (carrick#2056):
|
|
9380
9457
|
*
|
|
@@ -1228,6 +1228,14 @@ export interface InferredType {
|
|
|
1228
1228
|
* Absent when the body is not a raw-text read.
|
|
1229
1229
|
*/
|
|
1230
1230
|
raw_text_read?: true;
|
|
1231
|
+
/**
|
|
1232
|
+
* carrick#2814, a route's request body or a subscriber's payload: the
|
|
1233
|
+
* handler is in reach and works when nothing is sent, because it defaults
|
|
1234
|
+
* the body or reads it only in ways `undefined` survives (`?.`, `?? {}`).
|
|
1235
|
+
* Absent when the handler dereferences it, reads it through something not
|
|
1236
|
+
* seen, or was not found.
|
|
1237
|
+
*/
|
|
1238
|
+
absent_body_tolerated?: true;
|
|
1231
1239
|
/**
|
|
1232
1240
|
* carrick#2054, a response joined from two or more success bodies: the
|
|
1233
1241
|
* request fields whose values pick among them (`reads`), and, for one
|