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.
@@ -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
- private handsInAnotherFunction;
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
- return this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(target.node), undefined, this.primaryTypeSymbol(anchor.element), anchor.depth, this.primaryTypeSymbolSource(anchor.element));
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