carrick 0.3.125 → 0.3.127

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.
@@ -176,6 +176,12 @@ const RESPONSE_INIT_MEMBER_NAMES = new Set([
176
176
  * two; the bound is a backstop against a pathologically nested return.
177
177
  */
178
178
  const RESPONSE_HELPER_MAX_DEPTH = 4;
179
+ /**
180
+ * How many wrapper calls a handler bound to one is followed through
181
+ * (carrick#2472 P3-2b): `wrap(trace(async (req, res) => …))` needs two. A
182
+ * backstop against a pathologically nested initializer.
183
+ */
184
+ const WRAPPER_CALL_MAX_DEPTH = 3;
179
185
  /**
180
186
  * Members an options object uses to state an HTTP status. A >= 400 status marks
181
187
  * the branch an error path, whose shape is not the endpoint's contract.
@@ -190,6 +196,18 @@ const SERIALISED_BODY_UNREAD_DETAIL = {
190
196
  };
191
197
  /** `any` or `unknown` as a type in printed type text, not as a member name or a string. */
192
198
  const TOP_TYPE_TOKEN = /(?<![\w$'"`])(?:any|unknown)(?![\w$'"`?:])/;
199
+ /**
200
+ * The type an assertion states, or undefined when it states none. A const
201
+ * assertion (`x as const`, `<const>x`) narrows its operand's own type and
202
+ * names no type: its type node prints as `const`, a name nothing declares
203
+ * (carrick#2590). The walk stops there rather than reaching for an assertion
204
+ * further out, which describes a larger value than the read.
205
+ */
206
+ function statedAssertionType(typeNode) {
207
+ if (!typeNode || ts.isConstTypeReference(typeNode.compilerNode))
208
+ return undefined;
209
+ return typeNode;
210
+ }
193
211
  /**
194
212
  * The items whose whitespace-collapsed texts differ, first of each kept, in
195
213
  * order: how a joined union drops a member it already has.
@@ -608,13 +626,19 @@ export class TypeInferrer {
608
626
  * read is the project the scan loaded.
609
627
  *
610
628
  * Nothing is read past `deadline`: a batch that runs out keeps the readings
611
- * it made and publishes none for the rest.
629
+ * it made and publishes none for the rest, and marks each of the rest
630
+ * `unwidened_out_of_budget` (carrick#2100). A request that never needed a
631
+ * reading (a declared return, nothing to mark) or lost it for a reason of
632
+ * its own (a marking that added a diagnostic) is never marked.
612
633
  */
613
634
  addUnwidenedReadings(responses, extractionConfig, deadline) {
614
- const outOfTime = (stage) => {
635
+ const outOfTime = (stage, unread) => {
615
636
  if (performance.now() <= deadline)
616
637
  return false;
617
- this.log(`Unwidened reading stopped ${stage}: it ran out of its budget`);
638
+ this.log(`Unwidened reading stopped ${stage}: it ran out of its budget, ` +
639
+ `${unread.length} reading(s) not made`);
640
+ for (const plan of unread)
641
+ plan.result.unwidened_out_of_budget = true;
618
642
  return true;
619
643
  };
620
644
  const plans = [];
@@ -673,7 +697,7 @@ export class TypeInferrer {
673
697
  insertions.set(file, normalise(list));
674
698
  const before = new Map();
675
699
  for (const file of checkedFiles) {
676
- if (outOfTime('before the rewrite'))
700
+ if (outOfTime('before the rewrite', plans))
677
701
  return;
678
702
  before.set(file, fileDiagnostics(file));
679
703
  }
@@ -689,7 +713,7 @@ export class TypeInferrer {
689
713
  // holds it loses its reading.
690
714
  const tainted = new Set();
691
715
  for (const file of checkedFiles) {
692
- if (outOfTime('while checking the rewrite'))
716
+ if (outOfTime('while checking the rewrite', plans))
693
717
  return;
694
718
  const list = insertions.get(file) ?? [];
695
719
  for (const at of addedDiagnostics(before.get(file), fileDiagnostics(file), list)) {
@@ -701,9 +725,10 @@ export class TypeInferrer {
701
725
  }
702
726
  }
703
727
  }
704
- for (const plan of plans) {
705
- if (outOfTime('while re-reading'))
728
+ for (const [at, plan] of plans.entries()) {
729
+ if (outOfTime('while re-reading', plans.slice(at).filter((rest) => !tainted.has(rest)))) {
706
730
  return;
731
+ }
707
732
  if (tainted.has(plan)) {
708
733
  this.log(`Unwidened reading for ${plan.request.file_path}:${plan.request.line_number} ` +
709
734
  'dropped: marking its literals added a diagnostic on its path');
@@ -889,6 +914,16 @@ export class TypeInferrer {
889
914
  if (this.serialisedBodyEnvelopeEngages(awaitedType, func)) {
890
915
  return this.inferSerialisedBodyEnvelope(request, func);
891
916
  }
917
+ // A route handler that returns the reply it was handed (`return reply`)
918
+ // states no body by that return; the reply's type is built from the
919
+ // route's request type argument (carrick-cloud#2576).
920
+ const returned = this.returnedExpressions(func);
921
+ if (returned.length > 0 &&
922
+ returned.every((expression) => this.isRouteHandlerParameter(this.unwrapExpressionNode(expression), request))) {
923
+ this.log(`Return at ${request.file_path}:${request.line_number} is an object the ` +
924
+ 'framework handed the handler; it states no body');
925
+ return null;
926
+ }
892
927
  const unwrapResult = this.unwrapTypeWithConfig(awaitedType, func, extractionConfig);
893
928
  if (unwrapResult.wasUnwrapped && !unwrapResult.verifiedMachinery) {
894
929
  typeString = unwrapResult.typeString;
@@ -1266,6 +1301,16 @@ export class TypeInferrer {
1266
1301
  payloadNode = args[0];
1267
1302
  }
1268
1303
  }
1304
+ // A parameter the route registration hands its handler is never the
1305
+ // route's body: not the reply a handler returns (`return reply`), and not
1306
+ // the request the drill reaches in a call that forwards it
1307
+ // (`return finish(request, reply)`), whose type the framework built from
1308
+ // the route's request type argument (carrick-cloud#2576).
1309
+ if (this.isRouteHandlerParameter(payloadNode, request)) {
1310
+ this.log(`Response payload at ${request.file_path}:${request.line_number} is an object ` +
1311
+ 'the framework handed the handler; it states no body');
1312
+ return null;
1313
+ }
1269
1314
  const payloadType = payloadNode.getType();
1270
1315
  let typeString = typeText(payloadType, payloadNode);
1271
1316
  const unwrapResult = this.unwrapTypeWithConfig(payloadType, payloadNode, extractionConfig);
@@ -1435,6 +1480,30 @@ export class TypeInferrer {
1435
1480
  }
1436
1481
  return this.receivingCallOf(located, isReturned);
1437
1482
  }
1483
+ /**
1484
+ * True when `node` is a bare reference to a parameter of the handler the
1485
+ * route registration on the request's line hands in (found as
1486
+ * `registrationAtLine` finds it, wrappers included): the request, the reply
1487
+ * or the context the framework passes, never a body the route sends
1488
+ * (carrick-cloud#2576). Where the parameter's type is declared is not read:
1489
+ * a parameter of any other function is data, whether the repo or an
1490
+ * installed package types it (`.then((account) => reply.send(account))`, a
1491
+ * helper's `account: Account`).
1492
+ */
1493
+ isRouteHandlerParameter(node, request) {
1494
+ if (!Node.isIdentifier(node))
1495
+ return false;
1496
+ const owners = (node.getSymbol()?.getDeclarations() ?? [])
1497
+ .filter((decl) => Node.isParameterDeclaration(decl))
1498
+ .map((decl) => decl.getParent().compilerNode);
1499
+ if (owners.length === 0)
1500
+ return false;
1501
+ const sourceFile = this.project.getSourceFile(request.file_path);
1502
+ if (!sourceFile)
1503
+ return false;
1504
+ const handler = this.registrationAtLine(sourceFile, request.line_number)?.handler;
1505
+ return handler !== undefined && owners.includes(handler.compilerNode);
1506
+ }
1438
1507
  /**
1439
1508
  * True when a located call's own result is the route's payload, so the
1440
1509
  * transitional drill into its first argument must not run (carrick#1732).
@@ -2553,7 +2622,24 @@ export class TypeInferrer {
2553
2622
  // is its response, and a config's type is the config, so either one read
2554
2623
  // as the body publishes the wrong contract. Follow the call's signature to
2555
2624
  // the argument it sends as the body.
2556
- const inCall = this.requestBodyInCall(unwrapped);
2625
+ let inCall = this.requestBodyInCall(unwrapped);
2626
+ if (inCall.kind === 'none') {
2627
+ // carrick#2628: the argument goes into a function that builds the body
2628
+ // itself. An abstain here is a decision: capture's own re-read of the
2629
+ // locator would publish the argument as the body.
2630
+ const built = this.bodyBuiltByCallee(unwrapped);
2631
+ if (built.kind === 'abstain') {
2632
+ this.log(`Request locator at ${request.file_path}:${request.line_number}: ${built.why}; ` +
2633
+ 'the argument is not the body');
2634
+ return this.decidedAbstain(request, this.getNodeLocation(unwrapped), {
2635
+ path: '',
2636
+ kind: 'unknown',
2637
+ reason: 'body_built_by_callee',
2638
+ detail: `the located argument is not the request body: ${built.why}`,
2639
+ });
2640
+ }
2641
+ inCall = built;
2642
+ }
2557
2643
  if (inCall.kind === 'body') {
2558
2644
  this.log(`Request locator at ${request.file_path}:${request.line_number} names ${inCall.from}; ` +
2559
2645
  'reading the body argument its signature declares');
@@ -2736,9 +2822,14 @@ export class TypeInferrer {
2736
2822
  // (res) => resolve(…)) })`): what the caller receives is what that
2737
2823
  // promise settles with, and the call made in the executor produces it.
2738
2824
  const nearestReturn = callExpr.getFirstAncestorByKind(SyntaxKind.ReturnStatement);
2825
+ // A return whose value is an object or array literal built around the
2826
+ // call (`return { ok: true, count: countOf(await client.add(…)) }`) hands
2827
+ // back the caller's own value, which holds the result as one part: it
2828
+ // answers for the call no more than a command table does (carrick#2628).
2739
2829
  const returnStmt = nearestReturn &&
2740
2830
  (this.findContainingFunctionForNode(nearestReturn) === func ||
2741
- this.callIsInReturnedPromiseExecutor(nearestReturn, func))
2831
+ this.callIsInReturnedPromiseExecutor(nearestReturn, func)) &&
2832
+ !this.callIsPartOfReturnedLiteral(callExpr, nearestReturn)
2742
2833
  ? nearestReturn
2743
2834
  : undefined;
2744
2835
  if (returnStmt) {
@@ -2752,8 +2843,9 @@ export class TypeInferrer {
2752
2843
  // the response, so the walk below has nothing to follow, and the call
2753
2844
  // itself would stand as the terminal and publish the transport object.
2754
2845
  // The read is the terminal, as it is when the walk finds it on a binding,
2755
- // and a call that takes the read and states what it returns says more
2756
- // than the read does (carrick#1382).
2846
+ // and a call that takes the read and states it (a schema, or a function
2847
+ // that returns it unchanged) says more than the read does (carrick#1382,
2848
+ // carrick#2595).
2757
2849
  const inPlaceRead = this.bodyReadOnCallValue(callExpr);
2758
2850
  if (inPlaceRead) {
2759
2851
  return {
@@ -2771,8 +2863,11 @@ export class TypeInferrer {
2771
2863
  /** The value itself was read: returned, passed, aliased, or body-read. */
2772
2864
  let sawWholeRead = false;
2773
2865
  // Where the call's own value states no type, a call that takes it whole
2774
- // is the first place the source states one, and the walk follows it
2775
- // (`parse(raw)` after `const raw = await readRaw()`, carrick#1375).
2866
+ // and states it (a schema, or a function that returns it unchanged) is
2867
+ // the first place the source states one, and the walk follows it
2868
+ // (`parse(raw)` after `const raw = await readRaw()`, carrick#1375). A
2869
+ // converter, or a declaration whose body cannot be read, states nothing
2870
+ // about the value (carrick#2595).
2776
2871
  const callValue = this.unwrapPromiseType(callExpr.getType());
2777
2872
  const callValueIsUntyped = callValue.isAny() || callValue.isUnknown();
2778
2873
  // A transport response handed whole to a helper is a body read through
@@ -2871,7 +2966,7 @@ export class TypeInferrer {
2871
2966
  // body, and answers as it did.
2872
2967
  if (this.expressionUsesNames(expr, currentNames) &&
2873
2968
  !this.usesNamesOnlyByProjection(expr, currentNames)) {
2874
- if (callValueIsUntyped ||
2969
+ if ((callValueIsUntyped && this.statesTrackedValue(expr, currentNames)) ||
2875
2970
  this.restatesTrackedValue(expr, currentNames) ||
2876
2971
  this.holdsBinaryBodyRead(expr, currentNames) ||
2877
2972
  (callValueIsTransport && this.handsTrackedValueToACall(expr, currentNames))) {
@@ -2907,18 +3002,51 @@ export class TypeInferrer {
2907
3002
  }
2908
3003
  return callExpr;
2909
3004
  }
3005
+ /**
3006
+ * Between `callExpr` and the return statement around it there is an object
3007
+ * or array literal: the call's result is one part of a value the function
3008
+ * builds and returns.
3009
+ */
3010
+ callIsPartOfReturnedLiteral(callExpr, returnStmt) {
3011
+ const ancestors = callExpr.getAncestors();
3012
+ return ancestors
3013
+ .slice(0, ancestors.indexOf(returnStmt))
3014
+ .some((a) => Node.isObjectLiteralExpression(a) || Node.isArrayLiteralExpression(a));
3015
+ }
2910
3016
  /**
2911
3017
  * `expr` declares a name for what a call returns when it is handed a
2912
3018
  * tracked name whole (`const data: T = await parseJson(response, 'op')`).
2913
3019
  */
2914
3020
  handsTrackedValueToACall(expr, names) {
2915
- if (!Node.isVariableDeclaration(expr))
2916
- return false;
2917
- const init = this.unwrapExpressionNode(expr.getInitializer());
2918
- return (Node.isCallExpression(init) &&
2919
- init
2920
- .getArguments()
2921
- .some((argument) => this.isIdentifierUsage(this.unwrapExpressionNode(argument), names)));
3021
+ return Node.isVariableDeclaration(expr) && this.callTakingTrackedValue(expr, names) !== undefined;
3022
+ }
3023
+ /**
3024
+ * The call whose result `expr` declares or assigns a name for, and the
3025
+ * index of its argument that is a tracked name whole, or undefined.
3026
+ */
3027
+ callTakingTrackedValue(expr, names) {
3028
+ const value = Node.isVariableDeclaration(expr)
3029
+ ? expr.getInitializer()
3030
+ : Node.isBinaryExpression(expr) && expr.getOperatorToken().getKind() === SyntaxKind.EqualsToken
3031
+ ? expr.getRight()
3032
+ : undefined;
3033
+ const init = this.unwrapExpressionNode(value);
3034
+ if (!Node.isCallExpression(init))
3035
+ return undefined;
3036
+ const index = init
3037
+ .getArguments()
3038
+ .findIndex((argument) => this.isIdentifierUsage(this.unwrapExpressionNode(argument), names));
3039
+ return index >= 0 ? { call: init, index } : undefined;
3040
+ }
3041
+ /**
3042
+ * `expr` names the result of a call that states the tracked value it takes
3043
+ * (`callStatesItsArgument`): a schema validates it, or the callee returns it
3044
+ * unchanged. A converter's result describes what it built from the value,
3045
+ * not the value (carrick#2595).
3046
+ */
3047
+ statesTrackedValue(expr, names) {
3048
+ const taking = this.callTakingTrackedValue(expr, names);
3049
+ return !!taking && this.callStatesItsArgument(taking.call, taking.index);
2922
3050
  }
2923
3051
  /** `expr` holds a binary whole-body read taken on a tracked name. */
2924
3052
  holdsBinaryBodyRead(expr, names) {
@@ -3505,12 +3633,18 @@ export class TypeInferrer {
3505
3633
  * serialiser answering `string` describes what the caller did with the
3506
3634
  * body, not what the body is, and publishing it would be a
3507
3635
  * concrete-but-wrong contract where the honest `any` of the read is
3508
- * merely unresolved.
3636
+ * merely unresolved;
3637
+ * - the call states its argument (`callStatesItsArgument`): a schema
3638
+ * validates it, or the callee returns it unchanged. A converter's
3639
+ * object result describes what it built, not the body (carrick#2595).
3509
3640
  */
3510
3641
  statedPayloadAroundBodyRead(bodyRead) {
3511
3642
  return this.receivingCallOf(bodyRead, (candidate) => Node.isCallExpression(candidate) &&
3512
3643
  this.callResultIsBound(candidate) &&
3513
- this.isObjectShape(this.unwrapPromiseType(candidate.getType())));
3644
+ this.isObjectShape(this.unwrapPromiseType(candidate.getType())) &&
3645
+ this.callStatesItsArgument(candidate, candidate
3646
+ .getArguments()
3647
+ .findIndex((argument) => argument.getStart() <= bodyRead.getStart() && bodyRead.getEnd() <= argument.getEnd())));
3514
3648
  }
3515
3649
  /**
3516
3650
  * The source keeps this call's result: it initializes a declaration, is
@@ -4515,22 +4649,14 @@ export class TypeInferrer {
4515
4649
  const asExpr = Node.isAsExpression(node)
4516
4650
  ? node
4517
4651
  : node.getFirstAncestorByKind(SyntaxKind.AsExpression);
4518
- if (asExpr) {
4519
- const typeNode = asExpr.getTypeNode();
4520
- if (typeNode) {
4521
- return typeNode;
4522
- }
4652
+ const asType = statedAssertionType(asExpr?.getTypeNode());
4653
+ if (asType) {
4654
+ return asType;
4523
4655
  }
4524
4656
  const typeAssertion = Node.isTypeAssertion(node)
4525
4657
  ? node
4526
4658
  : node.getFirstAncestorByKind(SyntaxKind.TypeAssertionExpression);
4527
- if (typeAssertion) {
4528
- const typeNode = typeAssertion.getTypeNode();
4529
- if (typeNode) {
4530
- return typeNode;
4531
- }
4532
- }
4533
- return undefined;
4659
+ return statedAssertionType(typeAssertion?.getTypeNode());
4534
4660
  }
4535
4661
  /**
4536
4662
  * Render an explicit annotation (`as T`, `<T>`, or a typed binding) as
@@ -6236,14 +6362,17 @@ export class TypeInferrer {
6236
6362
  return undefined;
6237
6363
  }
6238
6364
  /**
6239
- * The handler function referenced by a route-registration call: the first
6365
+ * The handler function referenced by a route-registration call: the LAST
6240
6366
  * argument that is an inline function, or an identifier bound to a function
6241
- * (typically the 2nd+ arg — the path literal is not function-valued, so it is
6242
- * skipped naturally). No callee-name check: a call whose argument resolves to
6243
- * a function is structurally a registration regardless of the framework.
6367
+ * (carrick#2472 P3-2b). The functions before it are middleware
6368
+ * (`route(path).post(auth, audit, handler)`), which runs first and hands on;
6369
+ * reading the first function read a middleware as the route. The path
6370
+ * literal is not function-valued, so it is skipped naturally. No callee-name
6371
+ * check: a call whose argument resolves to a function is structurally a
6372
+ * registration regardless of the framework.
6244
6373
  */
6245
6374
  handlerFromRegistrationCall(call) {
6246
- for (const arg of call.getArguments()) {
6375
+ for (const arg of [...call.getArguments()].reverse()) {
6247
6376
  const handler = this.asHandlerFunction(arg);
6248
6377
  if (handler) {
6249
6378
  return handler;
@@ -6282,15 +6411,17 @@ export class TypeInferrer {
6282
6411
  * declaration (via the compiler's definition nodes) and returned when that
6283
6412
  * declaration is — or initializes to — a function. Anything else (a path
6284
6413
  * literal, an object, a non-function binding) yields undefined.
6414
+ * `followWrapperCalls` false reads a binding to a wrapper call as no
6415
+ * function, so a name the wrapper walk meets is never walked again.
6285
6416
  */
6286
- asHandlerFunction(node) {
6417
+ asHandlerFunction(node, followWrapperCalls = true) {
6287
6418
  const expr = this.unwrapExpressionNode(node);
6288
6419
  if (Node.isArrowFunction(expr) || Node.isFunctionExpression(expr)) {
6289
6420
  return expr;
6290
6421
  }
6291
6422
  if (Node.isIdentifier(expr)) {
6292
6423
  for (const def of expr.getDefinitionNodes()) {
6293
- const func = this.functionFromDeclaration(def);
6424
+ const func = this.functionFromDeclaration(def, followWrapperCalls);
6294
6425
  if (func) {
6295
6426
  return func;
6296
6427
  }
@@ -6302,10 +6433,13 @@ export class TypeInferrer {
6302
6433
  * Extract a `FunctionLike` from a declaration node the compiler resolved an
6303
6434
  * identifier to: a function declaration is itself the function; a variable
6304
6435
  * declaration / binding whose initializer is an inline function yields that
6305
- * function (`const h = async () => { … }`). Import/re-export shims are walked
6306
- * by ts-morph's `getDefinitionNodes`, so no manual import chasing is needed.
6436
+ * function (`const h = async () => { … }`), and one whose initializer is a
6437
+ * wrapper call yields the function the call wraps
6438
+ * (`const h = wrap<Contract>(async (req, res) => { … })`, carrick#2472
6439
+ * P3-2b; `functionWrappedBy`). Import/re-export shims are walked by
6440
+ * ts-morph's `getDefinitionNodes`, so no manual import chasing is needed.
6307
6441
  */
6308
- functionFromDeclaration(decl) {
6442
+ functionFromDeclaration(decl, followWrapperCalls = true) {
6309
6443
  if (Node.isFunctionDeclaration(decl)) {
6310
6444
  return decl;
6311
6445
  }
@@ -6330,10 +6464,43 @@ export class TypeInferrer {
6330
6464
  if (Node.isArrowFunction(inner) || Node.isFunctionExpression(inner)) {
6331
6465
  return inner;
6332
6466
  }
6467
+ if (followWrapperCalls && Node.isCallExpression(inner)) {
6468
+ return this.functionWrappedBy(inner, 0);
6469
+ }
6333
6470
  }
6334
6471
  }
6335
6472
  return undefined;
6336
6473
  }
6474
+ /**
6475
+ * The function a call wraps, for a name bound to that call and used as a
6476
+ * handler (carrick#2472 P3-2b): the call's one argument that is a function
6477
+ * literal, a name bound to one, or another such call, a few calls deep.
6478
+ *
6479
+ * Two conditions keep this to wrappers. The call's own value has to be
6480
+ * callable, because the name is used as a function: `rows.map((row) => …)`
6481
+ * is handed a function and yields a list. And exactly one argument may be a
6482
+ * function: with two there is no saying which of them is wrapped.
6483
+ */
6484
+ functionWrappedBy(call, depth) {
6485
+ if (depth >= WRAPPER_CALL_MAX_DEPTH)
6486
+ return undefined;
6487
+ if (!this.isCallableType(call.getType()))
6488
+ return undefined;
6489
+ const wrapped = [];
6490
+ for (const arg of call.getArguments()) {
6491
+ const value = this.unwrapExpressionNode(arg);
6492
+ const func = Node.isArrowFunction(value) || Node.isFunctionExpression(value)
6493
+ ? value
6494
+ : Node.isCallExpression(value)
6495
+ ? this.functionWrappedBy(value, depth + 1)
6496
+ : Node.isIdentifier(value)
6497
+ ? this.asHandlerFunction(value, false)
6498
+ : undefined;
6499
+ if (func)
6500
+ wrapped.push(func);
6501
+ }
6502
+ return wrapped.length === 1 ? wrapped[0] : undefined;
6503
+ }
6337
6504
  /**
6338
6505
  * True when `handler` reads nothing from the incoming message (carrick#827):
6339
6506
  * every use of one of its parameters, named or destructured, is the receiver
@@ -6951,6 +7118,321 @@ export class TypeInferrer {
6951
7118
  }
6952
7119
  return { kind: 'none' };
6953
7120
  }
7121
+ /**
7122
+ * carrick#2628: the located node is an argument of a call into a function
7123
+ * whose body the program holds (an in-repo client method,
7124
+ * `client.addTags(project, unique(tags))`), at a parameter that is not a
7125
+ * body slot. What the call sends is decided inside that function, so the
7126
+ * argument is the body only when the function sends it unchanged. One
7127
+ * level, read off each use of the parameter in the function's body:
7128
+ *
7129
+ * - the use IS the value of a body-named member (`body: tags`,
7130
+ * `body: JSON.stringify(tags)`) or of a body slot of a call: the
7131
+ * argument is the body, and its own type stands (`none`);
7132
+ * - the use sits inside such a value (`body: { tags }`,
7133
+ * `body: (event ?? {}) as never`): the body is that value, read at the
7134
+ * function without the casts around it;
7135
+ * - the use sits inside an object or array literal that is no body value
7136
+ * (`send(project, [labels])`): the body cannot be told, abstain.
7137
+ *
7138
+ * Uses that disagree abstain. A generic function's body may be typed by
7139
+ * its type parameters, so it abstains too. A library's declaration
7140
+ * (`fetch`, an installed client) has no body to read and is left alone,
7141
+ * and so is an untyped argument (a handler's `request.body` handed to a
7142
+ * helper): it states no contract to mistake, and the untyped-read path
7143
+ * below reads it.
7144
+ */
7145
+ bodyBuiltByCallee(node) {
7146
+ const read = this.calleeBodyRead(node, 0);
7147
+ return read.kind === 'passes' ? { kind: 'none' } : read;
7148
+ }
7149
+ /**
7150
+ * `bodyBuiltByCallee`, telling apart a callee that sends the argument
7151
+ * unchanged (`passes`) from one that states nothing about it (`none`).
7152
+ * `depth` counts the forwarded locals followed (carrick#2671).
7153
+ */
7154
+ calleeBodyRead(node, depth) {
7155
+ const own = node.getType();
7156
+ if (own.isAny() || own.isUnknown())
7157
+ return { kind: 'none' };
7158
+ const call = this.receivingCallOf(node, (candidate) => Node.isCallExpression(candidate));
7159
+ if (!call || !Node.isCallExpression(call))
7160
+ return { kind: 'none' };
7161
+ const index = call
7162
+ .getArguments()
7163
+ .findIndex((a) => a.getPos() <= node.getPos() && node.getEnd() <= a.getEnd());
7164
+ if (index < 0)
7165
+ return { kind: 'none' };
7166
+ if (this.requestBodySlots(call)[index])
7167
+ return { kind: 'none' };
7168
+ const callee = this.project.getTypeChecker().getResolvedSignature(call)?.getDeclaration();
7169
+ if (!callee ||
7170
+ !(Node.isFunctionDeclaration(callee) ||
7171
+ Node.isMethodDeclaration(callee) ||
7172
+ Node.isArrowFunction(callee) ||
7173
+ Node.isFunctionExpression(callee))) {
7174
+ return { kind: 'none' };
7175
+ }
7176
+ const body = callee.getBody();
7177
+ if (!body)
7178
+ return { kind: 'none' };
7179
+ const program = this.project.getProgram().compilerObject;
7180
+ if (isExternalOrigin(program, callee.getSourceFile().compilerNode, this.repoRoot, externalImportsOf(this.project))) {
7181
+ return { kind: 'none' };
7182
+ }
7183
+ const param = callee.getParameters()[index];
7184
+ if (!param || param.isRestParameter())
7185
+ return { kind: 'none' };
7186
+ const paramName = param.getNameNode();
7187
+ if (!Node.isIdentifier(paramName))
7188
+ return { kind: 'none' };
7189
+ const paramSymbol = paramName.getSymbol()?.compilerSymbol;
7190
+ if (!paramSymbol)
7191
+ return { kind: 'none' };
7192
+ const checker = this.project.getTypeChecker().compilerObject;
7193
+ const uses = body.getDescendantsOfKind(SyntaxKind.Identifier).filter((id) => {
7194
+ const parent = id.getParent();
7195
+ const symbol = parent && Node.isShorthandPropertyAssignment(parent)
7196
+ ? checker.getShorthandAssignmentValueSymbol(parent.compilerNode)
7197
+ : id.getSymbol()?.compilerSymbol;
7198
+ return symbol === paramSymbol;
7199
+ });
7200
+ const from = 'a call into a function that builds the request body inside itself';
7201
+ let passedThrough = false;
7202
+ const built = [];
7203
+ for (const use of uses) {
7204
+ // carrick#2668: `data ? JSON.stringify(data) : undefined` tests the
7205
+ // argument before it sends it; the test is no body.
7206
+ if (this.inSendNothingTest(use, body))
7207
+ continue;
7208
+ const value = this.bodyValueAround(use, body);
7209
+ if (value === use) {
7210
+ passedThrough = true;
7211
+ continue;
7212
+ }
7213
+ if (value) {
7214
+ built.push(value);
7215
+ continue;
7216
+ }
7217
+ // carrick#2671: the use builds a local (`const payload = { ...input,
7218
+ // code: norm(input.code) }`) the function then sends unchanged, itself
7219
+ // or through a helper that passes it on as the body. The body is that
7220
+ // local.
7221
+ const forwarded = this.forwardedLocalHolding(use, body, depth);
7222
+ if (forwarded) {
7223
+ built.push(forwarded);
7224
+ continue;
7225
+ }
7226
+ const ancestors = use.getAncestors();
7227
+ const insideLiteral = ancestors
7228
+ .slice(0, ancestors.indexOf(body))
7229
+ .some((a) => Node.isObjectLiteralExpression(a) || Node.isArrayLiteralExpression(a));
7230
+ if (insideLiteral) {
7231
+ return {
7232
+ kind: 'abstain',
7233
+ why: `the located argument goes into a value the called function builds and does not send as a body (${from})`,
7234
+ };
7235
+ }
7236
+ }
7237
+ if (built.length === 0)
7238
+ return passedThrough ? { kind: 'passes' } : { kind: 'none' };
7239
+ if (passedThrough) {
7240
+ return { kind: 'abstain', why: `the called function both sends the argument and builds a body from it (${from})` };
7241
+ }
7242
+ if (callee.getTypeParameters().length > 0) {
7243
+ return { kind: 'abstain', why: `the called function is generic, so the body it builds may be typed by its type parameters (${from})` };
7244
+ }
7245
+ const types = built.map((value) => this.sentValue(value));
7246
+ const texts = new Set(types.map((value) => typeText(value.getType(), value)));
7247
+ if (texts.size !== 1) {
7248
+ return { kind: 'abstain', why: `the called function builds more than one body from the argument (${from})` };
7249
+ }
7250
+ const at = types[0];
7251
+ const type = at.getType();
7252
+ if (type.isTypeParameter() || printedTypeHoldsTopType([...texts][0])) {
7253
+ return { kind: 'abstain', why: `the body the called function builds is not fully typed (${from})` };
7254
+ }
7255
+ return { kind: 'body_type', type, at, from };
7256
+ }
7257
+ /**
7258
+ * The request body value `use` is part of, inside `scope`: the value of a
7259
+ * body-named object member, or the argument at a body slot of a call,
7260
+ * looking through parentheses, casts, `await` and a `JSON.stringify`
7261
+ * around it. `use` itself when it is that value whole.
7262
+ */
7263
+ bodyValueAround(use, scope) {
7264
+ let current = use;
7265
+ while (current !== scope) {
7266
+ const parent = current.getParent();
7267
+ if (!parent)
7268
+ return undefined;
7269
+ if (Node.isShorthandPropertyAssignment(parent) &&
7270
+ current === use &&
7271
+ BODY_MEMBER_NAMES.has(parent.getName())) {
7272
+ return use;
7273
+ }
7274
+ if (Node.isPropertyAssignment(parent) &&
7275
+ parent.getInitializer() === current &&
7276
+ BODY_MEMBER_NAMES.has(parent.getName())) {
7277
+ return this.isSameValue(current, use) ? use : current;
7278
+ }
7279
+ if (Node.isCallExpression(parent)) {
7280
+ const index = parent.getArguments().indexOf(current);
7281
+ if (index >= 0 && this.requestBodySlots(parent)[index]?.kind === 'body') {
7282
+ return this.isSameValue(current, use) ? use : current;
7283
+ }
7284
+ }
7285
+ current = parent;
7286
+ }
7287
+ return undefined;
7288
+ }
7289
+ /**
7290
+ * carrick#2671: the `const` local whose initializer holds `use`, inside
7291
+ * `scope`, when `scope` sends that local unchanged as the body: whole in a
7292
+ * body slot, or as an argument into a function that passes it on as its
7293
+ * body. The local's name, so its declared type is read. Undefined when the
7294
+ * local is never sent, when a further body is built from it, or when a
7295
+ * function lies between `use` and the declaration.
7296
+ */
7297
+ forwardedLocalHolding(use, scope, depth) {
7298
+ if (depth >= 2)
7299
+ return undefined;
7300
+ let current = use;
7301
+ let declaration;
7302
+ while (current !== scope) {
7303
+ const parent = current.getParent();
7304
+ if (!parent)
7305
+ return undefined;
7306
+ if (Node.isArrowFunction(parent) ||
7307
+ Node.isFunctionExpression(parent) ||
7308
+ Node.isFunctionDeclaration(parent) ||
7309
+ Node.isMethodDeclaration(parent)) {
7310
+ return undefined;
7311
+ }
7312
+ if (Node.isVariableDeclaration(parent) && parent.getInitializer() === current) {
7313
+ declaration = parent;
7314
+ break;
7315
+ }
7316
+ current = parent;
7317
+ }
7318
+ if (!declaration)
7319
+ return undefined;
7320
+ if (declaration.getVariableStatement()?.getDeclarationKind() !== VariableDeclarationKind.Const) {
7321
+ return undefined;
7322
+ }
7323
+ const name = declaration.getNameNode();
7324
+ if (!Node.isIdentifier(name))
7325
+ return undefined;
7326
+ const symbol = name.getSymbol()?.compilerSymbol;
7327
+ if (!symbol)
7328
+ return undefined;
7329
+ const checker = this.project.getTypeChecker().compilerObject;
7330
+ let sent = false;
7331
+ for (const ref of scope.getDescendantsOfKind(SyntaxKind.Identifier)) {
7332
+ if (ref === name)
7333
+ continue;
7334
+ const parent = ref.getParent();
7335
+ const refSymbol = parent && Node.isShorthandPropertyAssignment(parent)
7336
+ ? checker.getShorthandAssignmentValueSymbol(parent.compilerNode)
7337
+ : ref.getSymbol()?.compilerSymbol;
7338
+ if (refSymbol !== symbol)
7339
+ continue;
7340
+ if (this.inSendNothingTest(ref, scope))
7341
+ continue;
7342
+ const value = this.bodyValueAround(ref, scope);
7343
+ if (value === ref) {
7344
+ sent = true;
7345
+ continue;
7346
+ }
7347
+ if (value)
7348
+ return undefined;
7349
+ if (parent && Node.isCallExpression(parent) && parent.getArguments().includes(ref)) {
7350
+ const onward = this.calleeBodyRead(ref, depth + 1);
7351
+ if (onward.kind === 'passes') {
7352
+ sent = true;
7353
+ continue;
7354
+ }
7355
+ if (onward.kind !== 'none')
7356
+ return undefined;
7357
+ }
7358
+ }
7359
+ return sent ? name : undefined;
7360
+ }
7361
+ /** `value` is `use` behind what `sentValue` sees through. */
7362
+ isSameValue(value, use) {
7363
+ return this.sentValue(value) === use;
7364
+ }
7365
+ /**
7366
+ * The value a body slot sends when it sends anything, behind parentheses,
7367
+ * casts, `await`, a `JSON.stringify`, and (carrick#2668) a choice whose
7368
+ * other side sends nothing: `x ? JSON.stringify(x) : undefined`,
7369
+ * `x || null`, `x ?? undefined`. A `?? {}` sends `{}`, so it is a body the
7370
+ * function builds and is not seen through.
7371
+ */
7372
+ sentValue(value) {
7373
+ let current = value;
7374
+ for (;;) {
7375
+ let inner = this.unwrapJsonStringifyArg(this.unwrapExpressionNode(current));
7376
+ if (Node.isConditionalExpression(inner)) {
7377
+ inner = this.sendingBranch(inner) ?? inner;
7378
+ }
7379
+ else if (Node.isBinaryExpression(inner)) {
7380
+ const operator = inner.getOperatorToken().getKind();
7381
+ if ((operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) &&
7382
+ this.sendsNothing(inner.getRight(), false)) {
7383
+ inner = inner.getLeft();
7384
+ }
7385
+ }
7386
+ if (inner === current)
7387
+ return current;
7388
+ current = inner;
7389
+ }
7390
+ }
7391
+ /**
7392
+ * The branch of `choice` that sends something, when its other branch sends
7393
+ * nothing (`undefined`, `null`, `void 0`, or `{}` spread into a request).
7394
+ */
7395
+ sendingBranch(choice) {
7396
+ const whenTrue = choice.getWhenTrue();
7397
+ const whenFalse = choice.getWhenFalse();
7398
+ if (this.sendsNothing(whenFalse, true) && !this.sendsNothing(whenTrue, true))
7399
+ return whenTrue;
7400
+ if (this.sendsNothing(whenTrue, true) && !this.sendsNothing(whenFalse, true))
7401
+ return whenFalse;
7402
+ return undefined;
7403
+ }
7404
+ /** `undefined`, `null`, `void 0`, or (where `emptyObject`) `{}`. */
7405
+ sendsNothing(node, emptyObject) {
7406
+ const value = this.unwrapExpressionNode(node);
7407
+ if (Node.isNullLiteral(value) || Node.isVoidExpression(value))
7408
+ return true;
7409
+ if (Node.isIdentifier(value) && value.getText() === 'undefined')
7410
+ return true;
7411
+ return (emptyObject &&
7412
+ Node.isObjectLiteralExpression(value) &&
7413
+ value.getProperties().length === 0);
7414
+ }
7415
+ /**
7416
+ * carrick#2668, #2664: `use` sits in the test of a choice whose other side
7417
+ * sends nothing (`body ? JSON.stringify(body) : null`,
7418
+ * `...(body === undefined ? {} : { body: JSON.stringify(body) })`). The
7419
+ * test only decides whether the argument is sent.
7420
+ */
7421
+ inSendNothingTest(use, scope) {
7422
+ let current = use;
7423
+ while (current !== scope) {
7424
+ const parent = current.getParent();
7425
+ if (!parent)
7426
+ return false;
7427
+ if (Node.isConditionalExpression(parent) &&
7428
+ parent.getCondition() === current &&
7429
+ this.sendingBranch(parent)) {
7430
+ return true;
7431
+ }
7432
+ current = parent;
7433
+ }
7434
+ return false;
7435
+ }
6954
7436
  /** A string, a string literal, a template literal, or a union of strings. */
6955
7437
  isStringLikeType(type) {
6956
7438
  return (type.isString() ||
@@ -7480,25 +7962,10 @@ export class TypeInferrer {
7480
7962
  * `reads.parameters`, the parameter's own type (rank 4).
7481
7963
  */
7482
7964
  stateAtReceivingCall(call, index, reads, found, stateNode) {
7483
- const schemaCandidates = [];
7484
- const callee = call.getExpression();
7485
- if (Node.isPropertyAccessExpression(callee))
7486
- schemaCandidates.push(callee.getExpression());
7487
- call.getArguments().forEach((argument, at) => {
7488
- if (at !== index)
7489
- schemaCandidates.push(this.unwrapExpressionNode(argument));
7490
- });
7491
- for (const candidate of schemaCandidates) {
7492
- const candidateType = candidate.getType();
7493
- if (!this.schemaOutputType(candidateType, candidate) &&
7494
- !this.schemaInputType(candidateType, candidate)) {
7495
- continue;
7496
- }
7497
- const contract = this.schemaContract(candidate, 'input');
7498
- if (contract) {
7499
- found.push({ rank: 1, text: contract.text, provenance: contract.provenance, at: call, how: 'a schema it is handed to' });
7500
- return;
7501
- }
7965
+ const contract = this.schemaHandedArgument(call, index);
7966
+ if (contract) {
7967
+ found.push({ rank: 1, text: contract.text, provenance: contract.provenance, at: call, how: 'a schema it is handed to' });
7968
+ return;
7502
7969
  }
7503
7970
  const passThrough = this.passThroughFunction(call, index);
7504
7971
  if (passThrough) {
@@ -7527,6 +7994,44 @@ export class TypeInferrer {
7527
7994
  return;
7528
7995
  found.push({ rank: 4, text, at: call, how: 'the parameter of a call it is handed to' });
7529
7996
  }
7997
+ /**
7998
+ * The input contract of a schema `call` validates its argument at `index`
7999
+ * with: its receiver (`Schema.safeParse(body)`) or one of its other
8000
+ * arguments (`parse(Schema, body)`), or null. A candidate counts only when
8001
+ * it declares schema types, so no method or library name is matched
8002
+ * (carrick#1166).
8003
+ */
8004
+ schemaHandedArgument(call, index) {
8005
+ const candidates = [];
8006
+ const callee = call.getExpression();
8007
+ if (Node.isPropertyAccessExpression(callee))
8008
+ candidates.push(callee.getExpression());
8009
+ call.getArguments().forEach((argument, at) => {
8010
+ if (at !== index)
8011
+ candidates.push(this.unwrapExpressionNode(argument));
8012
+ });
8013
+ for (const candidate of candidates) {
8014
+ const candidateType = candidate.getType();
8015
+ if (!this.schemaOutputType(candidateType, candidate) &&
8016
+ !this.schemaInputType(candidateType, candidate)) {
8017
+ continue;
8018
+ }
8019
+ const contract = this.schemaContract(candidate, 'input');
8020
+ if (contract)
8021
+ return contract;
8022
+ }
8023
+ return null;
8024
+ }
8025
+ /**
8026
+ * `call`'s result describes the value it takes at argument `index`: it
8027
+ * validates it with a schema, or its callee returns it unchanged
8028
+ * (`passThroughFunction`). Any other call builds something new from the
8029
+ * value, and its result describes what it built, never the value it was
8030
+ * handed (carrick#2595).
8031
+ */
8032
+ callStatesItsArgument(call, index) {
8033
+ return index >= 0 && (!!this.schemaHandedArgument(call, index) || !!this.passThroughFunction(call, index));
8034
+ }
7530
8035
  /**
7531
8036
  * The project function `call` invokes, when every value it returns is its
7532
8037
  * argument at `index` as it was handed in (through parentheses, casts,