carrick 0.3.126 → 0.3.128

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,6 +31,7 @@ import { functionAtLine } from './function-line-index.js';
31
31
  import { attributedDeclaration, installedPackageOf } from './declaration-attribution.js';
32
32
  import { endLineOf, startLineOf } from './line-index.js';
33
33
  import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
34
+ import { StatedParams } from './stated-params.js';
34
35
  import { classifyStatusCodes, constituents, httpStatus, statesNoBody, statesNoShape, statusKeyCode, statusTableOf, successEntries, } from './capture/index.js';
35
36
  import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
36
37
  import { expandTypeStructural, jsonWireType, } from './type-structural-expander.js';
@@ -176,11 +177,29 @@ const RESPONSE_INIT_MEMBER_NAMES = new Set([
176
177
  * two; the bound is a backstop against a pathologically nested return.
177
178
  */
178
179
  const RESPONSE_HELPER_MAX_DEPTH = 4;
180
+ /**
181
+ * How many wrapper calls a handler bound to one is followed through
182
+ * (carrick#2472 P3-2b): `wrap(trace(async (req, res) => …))` needs two. A
183
+ * backstop against a pathologically nested initializer.
184
+ */
185
+ const WRAPPER_CALL_MAX_DEPTH = 3;
179
186
  /**
180
187
  * Members an options object uses to state an HTTP status. A >= 400 status marks
181
188
  * the branch an error path, whose shape is not the endpoint's contract.
182
189
  */
183
190
  const STATUS_MEMBER_NAMES = ['status', 'statusCode'];
191
+ /**
192
+ * The reasons an `unknown` answer is a decision, not a failure to read: the
193
+ * list `DECIDED_ABSTAIN_REASONS` in `src/engine/type_compat_v2.rs` holds
194
+ * final. A stated reply never replaces one (carrick#2674).
195
+ */
196
+ const DECIDED_ABSTAIN_REASONS = new Set([
197
+ 'no_success_payload',
198
+ 'no_request_body',
199
+ 'projected_value_only',
200
+ 'serialised_body_unread',
201
+ 'body_built_by_callee',
202
+ ]);
184
203
  /** The provenance sentence for each reason a branch was not read. */
185
204
  const SERIALISED_BODY_UNREAD_DETAIL = {
186
205
  prebuilt: 'A return hands back a status and body built somewhere else, such as a variable or a field of another result, so the value in its body was not read.',
@@ -442,13 +461,16 @@ export class TypeInferrer {
442
461
  * @param onRequestDone - Called once per request, as the batch is done with it
443
462
  * @returns InferResult with inferred types or errors
444
463
  */
445
- infer(requests, extractionConfig, onRequestDone, modelSymbols) {
464
+ infer(requests, extractionConfig, onRequestDone, modelSymbols, labels) {
446
465
  const started = performance.now();
447
466
  this.modelSymbols = new Map((modelSymbols ?? []).map((symbol) => [symbol.alias, symbol]));
467
+ this.statedParams = this.statedParamsFor(labels ?? { sends: new Map(), bodies: new Set() });
448
468
  const inferredTypes = [];
449
469
  const errors = [];
450
470
  /** Response inferences the unwidened reading re-reads (carrick#1516). */
451
471
  const responses = [];
472
+ /** Request-body inferences it re-reads after them (carrick#2011). */
473
+ const bodies = [];
452
474
  /** How long each request took, in the order they were done (carrick#1985). */
453
475
  const timings = [];
454
476
  for (const request of requests) {
@@ -485,6 +507,9 @@ export class TypeInferrer {
485
507
  if (request.infer_kind === 'response_body' || request.infer_kind === 'function_return') {
486
508
  responses.push({ request, result });
487
509
  }
510
+ else if (request.infer_kind === 'request_body') {
511
+ bodies.push({ request, result });
512
+ }
488
513
  }
489
514
  else {
490
515
  errors.push(`Could not infer type at ${request.file_path}:${loc} (${request.infer_kind})`);
@@ -515,10 +540,47 @@ export class TypeInferrer {
515
540
  }
516
541
  try {
517
542
  const deadline = Math.min(performance.now() + this.unwidenedBudgetMs, started + UNWIDENED_LATEST_MS);
518
- this.addUnwidenedReadings(responses, extractionConfig, deadline);
543
+ // The responses' rewrite forgets every node of the files it touches, so
544
+ // each body's read node is kept as its span, taken now and found again
545
+ // in the restored file.
546
+ const bodySpans = bodies.map((body) => {
547
+ const node = this.readNodes.get(body.result.source_location);
548
+ return {
549
+ ...body,
550
+ readSpan: node && {
551
+ file: node.getSourceFile(),
552
+ start: node.getStart(),
553
+ end: node.getEnd(),
554
+ kind: node.getKind(),
555
+ },
556
+ };
557
+ });
558
+ try {
559
+ this.addUnwidenedReadings(responses, extractionConfig, deadline, 'response');
560
+ }
561
+ catch (err) {
562
+ // The reading only ever adds a field; it never costs an answer.
563
+ this.logError(`Unwidened reading failed: ${err instanceof Error ? err.message : String(err)}`);
564
+ }
565
+ try {
566
+ // Second, and in a rewrite of its own: the bodies' files never delay a
567
+ // response's reading, whose budget a spent deadline turns into an
568
+ // abstention (carrick#2100).
569
+ this.addUnwidenedReadings(bodySpans, extractionConfig, deadline, 'request_body');
570
+ }
571
+ catch (err) {
572
+ this.logError(`Unwidened reading of request bodies failed: ${err instanceof Error ? err.message : String(err)}`);
573
+ // carrick#2011: a body whose reading was not made is judged on its
574
+ // widened type alone, so it is marked the way a spent budget marks it
575
+ // and its pair abstains on a mismatch rather than report one.
576
+ for (const body of bodies) {
577
+ if (body.result.unwidened_type_string === undefined) {
578
+ body.result.unwidened_out_of_budget = true;
579
+ }
580
+ }
581
+ }
519
582
  }
520
583
  catch (err) {
521
- // The reading only ever adds a field; it never costs an answer.
522
584
  this.logError(`Unwidened reading failed: ${err instanceof Error ? err.message : String(err)}`);
523
585
  }
524
586
  return {
@@ -530,6 +592,12 @@ export class TypeInferrer {
530
592
  }
531
593
  /** The types the model named, by the alias of the row each is for (carrick#2111). */
532
594
  modelSymbols = new Map();
595
+ /**
596
+ * carrick#2674: what route handlers' parameters state, through the
597
+ * members the batch's verified server-framework labels name. Empty when
598
+ * the batch carries no labels, and then it reads nothing.
599
+ */
600
+ statedParams = this.statedParamsFor({ sends: new Map(), bodies: new Set() });
533
601
  /**
534
602
  * carrick#2111: where the model named a type for the response this request
535
603
  * reads, record on `inferred` how that type relates to the compiler's type
@@ -614,19 +682,29 @@ export class TypeInferrer {
614
682
  * really returns beside the published one (`unwidened_type_string`). See
615
683
  * `unwidened.ts` for what is marked and why the reading is sound.
616
684
  *
685
+ * carrick#2011: a request body (`read` is `'request_body'`) is read the same
686
+ * way, the call that sends it in place of the handler, except that a literal
687
+ * handed to any call other than the send is never marked.
688
+ *
617
689
  * One rewrite for the whole batch: every file on any request's path is
618
690
  * rewritten once, type-checked before and after, the requests re-inferred,
619
691
  * and every file restored before returning, so the project other requests
620
692
  * read is the project the scan loaded.
621
693
  *
622
694
  * Nothing is read past `deadline`: a batch that runs out keeps the readings
623
- * it made and publishes none for the rest.
695
+ * it made and publishes none for the rest, and marks each of the rest
696
+ * `unwidened_out_of_budget` (carrick#2100). A request that never needed a
697
+ * reading (a declared return, nothing to mark) or lost it for a reason of
698
+ * its own (a marking that added a diagnostic) is never marked.
624
699
  */
625
- addUnwidenedReadings(responses, extractionConfig, deadline) {
626
- const outOfTime = (stage) => {
700
+ addUnwidenedReadings(responses, extractionConfig, deadline, read) {
701
+ const outOfTime = (stage, unread) => {
627
702
  if (performance.now() <= deadline)
628
703
  return false;
629
- this.log(`Unwidened reading stopped ${stage}: it ran out of its budget`);
704
+ this.log(`Unwidened reading stopped ${stage}: it ran out of its budget, ` +
705
+ `${unread.length} reading(s) not made`);
706
+ for (const plan of unread)
707
+ plan.result.unwidened_out_of_budget = true;
630
708
  return true;
631
709
  };
632
710
  const plans = [];
@@ -634,17 +712,19 @@ export class TypeInferrer {
634
712
  const checkedFiles = new Set();
635
713
  // Every position is read before the first rewrite: a rewrite forgets
636
714
  // every node of its file.
637
- for (const { request, result } of responses) {
715
+ for (const { request, result, readSpan } of responses) {
638
716
  // A declared return type is the contract; nothing was widened.
639
717
  if (result.is_explicit)
640
718
  continue;
641
- const read = this.readNodes.get(result.source_location);
642
- if (!read)
719
+ const readNode = readSpan
720
+ ? nodeAtSpan(readSpan)
721
+ : this.readNodes.get(result.source_location);
722
+ if (!readNode)
643
723
  continue;
644
- const functions = pathOf(read);
724
+ const functions = pathOf(readNode);
645
725
  let marked = 0;
646
726
  for (const fn of functions) {
647
- const insertions = literalInsertions(fn);
727
+ const insertions = literalInsertions(fn, read === 'request_body' ? readNode : undefined);
648
728
  marked += insertions.length;
649
729
  if (insertions.length === 0)
650
730
  continue;
@@ -673,7 +753,7 @@ export class TypeInferrer {
673
753
  plans.push({
674
754
  request,
675
755
  result,
676
- read: { file: read.getSourceFile(), start: read.getStart(), end: read.getEnd() },
756
+ read: { file: readNode.getSourceFile(), start: readNode.getStart(), end: readNode.getEnd() },
677
757
  path,
678
758
  locatedByText,
679
759
  });
@@ -685,7 +765,7 @@ export class TypeInferrer {
685
765
  insertions.set(file, normalise(list));
686
766
  const before = new Map();
687
767
  for (const file of checkedFiles) {
688
- if (outOfTime('before the rewrite'))
768
+ if (outOfTime('before the rewrite', plans))
689
769
  return;
690
770
  before.set(file, fileDiagnostics(file));
691
771
  }
@@ -701,7 +781,7 @@ export class TypeInferrer {
701
781
  // holds it loses its reading.
702
782
  const tainted = new Set();
703
783
  for (const file of checkedFiles) {
704
- if (outOfTime('while checking the rewrite'))
784
+ if (outOfTime('while checking the rewrite', plans))
705
785
  return;
706
786
  const list = insertions.get(file) ?? [];
707
787
  for (const at of addedDiagnostics(before.get(file), fileDiagnostics(file), list)) {
@@ -713,9 +793,10 @@ export class TypeInferrer {
713
793
  }
714
794
  }
715
795
  }
716
- for (const plan of plans) {
717
- if (outOfTime('while re-reading'))
796
+ for (const [at, plan] of plans.entries()) {
797
+ if (outOfTime('while re-reading', plans.slice(at).filter((rest) => !tainted.has(rest)))) {
718
798
  return;
799
+ }
719
800
  if (tainted.has(plan)) {
720
801
  this.log(`Unwidened reading for ${plan.request.file_path}:${plan.request.line_number} ` +
721
802
  'dropped: marking its literals added a diagnostic on its path');
@@ -794,7 +875,7 @@ export class TypeInferrer {
794
875
  case 'function_return':
795
876
  return this.inferFunctionReturn(sourceFile, request, extractionConfig);
796
877
  case 'response_body':
797
- return this.inferResponseBody(sourceFile, request, extractionConfig);
878
+ return this.withStatedReply(sourceFile, request, this.inferResponseBody(sourceFile, request, extractionConfig));
798
879
  case 'call_result':
799
880
  return this.inferCallResult(sourceFile, request, extractionConfig);
800
881
  case 'variable':
@@ -901,6 +982,16 @@ export class TypeInferrer {
901
982
  if (this.serialisedBodyEnvelopeEngages(awaitedType, func)) {
902
983
  return this.inferSerialisedBodyEnvelope(request, func);
903
984
  }
985
+ // A route handler that returns the reply it was handed (`return reply`)
986
+ // states no body by that return; the reply's type is built from the
987
+ // route's request type argument (carrick-cloud#2576).
988
+ const returned = this.returnedExpressions(func);
989
+ if (returned.length > 0 &&
990
+ returned.every((expression) => this.isRouteHandlerParameter(this.unwrapExpressionNode(expression), request))) {
991
+ this.log(`Return at ${request.file_path}:${request.line_number} is an object the ` +
992
+ 'framework handed the handler; it states no body');
993
+ return null;
994
+ }
904
995
  const unwrapResult = this.unwrapTypeWithConfig(awaitedType, func, extractionConfig);
905
996
  if (unwrapResult.wasUnwrapped && !unwrapResult.verifiedMachinery) {
906
997
  typeString = unwrapResult.typeString;
@@ -1278,6 +1369,16 @@ export class TypeInferrer {
1278
1369
  payloadNode = args[0];
1279
1370
  }
1280
1371
  }
1372
+ // A parameter the route registration hands its handler is never the
1373
+ // route's body: not the reply a handler returns (`return reply`), and not
1374
+ // the request the drill reaches in a call that forwards it
1375
+ // (`return finish(request, reply)`), whose type the framework built from
1376
+ // the route's request type argument (carrick-cloud#2576).
1377
+ if (this.isRouteHandlerParameter(payloadNode, request)) {
1378
+ this.log(`Response payload at ${request.file_path}:${request.line_number} is an object ` +
1379
+ 'the framework handed the handler; it states no body');
1380
+ return null;
1381
+ }
1281
1382
  const payloadType = payloadNode.getType();
1282
1383
  let typeString = typeText(payloadType, payloadNode);
1283
1384
  const unwrapResult = this.unwrapTypeWithConfig(payloadType, payloadNode, extractionConfig);
@@ -1447,6 +1548,30 @@ export class TypeInferrer {
1447
1548
  }
1448
1549
  return this.receivingCallOf(located, isReturned);
1449
1550
  }
1551
+ /**
1552
+ * True when `node` is a bare reference to a parameter of the handler the
1553
+ * route registration on the request's line hands in (found as
1554
+ * `registrationAtLine` finds it, wrappers included): the request, the reply
1555
+ * or the context the framework passes, never a body the route sends
1556
+ * (carrick-cloud#2576). Where the parameter's type is declared is not read:
1557
+ * a parameter of any other function is data, whether the repo or an
1558
+ * installed package types it (`.then((account) => reply.send(account))`, a
1559
+ * helper's `account: Account`).
1560
+ */
1561
+ isRouteHandlerParameter(node, request) {
1562
+ if (!Node.isIdentifier(node))
1563
+ return false;
1564
+ const owners = (node.getSymbol()?.getDeclarations() ?? [])
1565
+ .filter((decl) => Node.isParameterDeclaration(decl))
1566
+ .map((decl) => decl.getParent().compilerNode);
1567
+ if (owners.length === 0)
1568
+ return false;
1569
+ const sourceFile = this.project.getSourceFile(request.file_path);
1570
+ if (!sourceFile)
1571
+ return false;
1572
+ const handler = this.registrationAtLine(sourceFile, request.line_number)?.handler;
1573
+ return handler !== undefined && owners.includes(handler.compilerNode);
1574
+ }
1450
1575
  /**
1451
1576
  * True when a located call's own result is the route's payload, so the
1452
1577
  * transitional drill into its first argument must not run (carrick#1732).
@@ -1852,6 +1977,16 @@ export class TypeInferrer {
1852
1977
  typeString = explicitType;
1853
1978
  isExplicit = true;
1854
1979
  }
1980
+ // carrick#2714: the body read is cast to a placeholder
1981
+ // (`(await res.json()) as { data: Item[] } | { error: unknown }`) and the
1982
+ // source narrows the value later (`return json as { data: Item[] }`).
1983
+ // The narrowed statement is the body, and it is reported as stated, so a
1984
+ // model symbol naming an element inside it is not kept as the body.
1985
+ const narrowed = func ? this.narrowedBodyStatement(terminalNode, func) : undefined;
1986
+ if (narrowed) {
1987
+ typeString = narrowed.text;
1988
+ isExplicit = true;
1989
+ }
1855
1990
  // carrick#1843: a rule can now match a thenable or a carrier by its alias,
1856
1991
  // so on a result that is a carrier (`Task<Outcome<Reply<T>, E>>`) the
1857
1992
  // rules reach what the carrier holds before the carrier read below does.
@@ -1863,6 +1998,17 @@ export class TypeInferrer {
1863
1998
  const callArgs = this.callResultArguments(callExpr);
1864
1999
  const resultIsCarrier = !explicitType &&
1865
2000
  this.resultCarrierArguments(returnType, terminalNode, callArgs) !== undefined;
2001
+ // carrick#2714, carrick#1841: the call returns a request library's
2002
+ // response object, the body in a member around a status, headers and the
2003
+ // request's config. The body is what the source reads that member as, and
2004
+ // never the object: judged against a producer's body, every transport
2005
+ // member reads as one the producer does not send.
2006
+ if (!explicitType && !resultIsCarrier && func) {
2007
+ const transport = this.transportRuleOfCall(callExpr, extractionConfig);
2008
+ if (transport && !this.callStatesTypedPayload(callExpr, extractionConfig)) {
2009
+ return this.transportCallBody(request, callExpr, func, transport.rule, where);
2010
+ }
2011
+ }
1866
2012
  // carrick#1877: the terminal holds a thenable the names do not peel, a
1867
2013
  // subclass of `Promise` or a class with a `then` of its own. What a
1868
2014
  // caller receives is what `await` yields for it, as it is for the
@@ -2076,7 +2222,9 @@ export class TypeInferrer {
2076
2222
  const leftCarrier = (callUnwrap.wasUnwrapped || readByProtocol) &&
2077
2223
  !!callAfterRules &&
2078
2224
  this.resultCarrierArguments(callAfterRules, callExpr, callArgs) !== undefined;
2079
- const anchorSource = carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
2225
+ const anchorSource = narrowed?.typeNode
2226
+ ? narrowed.typeNode.getType()
2227
+ : carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
2080
2228
  let anchor = anchorSource
2081
2229
  ? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
2082
2230
  : undefined;
@@ -2125,7 +2273,11 @@ export class TypeInferrer {
2125
2273
  // carrick#1749: say when the text is the source's own statement of the
2126
2274
  // body, and what that statement is rooted at, so the scanner can tell a
2127
2275
  // model symbol that names the body from one that names a part of it.
2128
- const statedBody = explicitType ? this.statedBodyAtRead(terminalNode) : undefined;
2276
+ const statedBody = narrowed?.typeNode
2277
+ ? this.statedRoot(narrowed.typeNode)
2278
+ : explicitType
2279
+ ? this.statedBodyAtRead(terminalNode)
2280
+ : undefined;
2129
2281
  if (statedBody) {
2130
2282
  inferred.stated_body = statedBody;
2131
2283
  }
@@ -3403,6 +3555,243 @@ export class TypeInferrer {
3403
3555
  }
3404
3556
  return common;
3405
3557
  }
3558
+ /**
3559
+ * What the source narrows a call's value to, where the type stated at the
3560
+ * walk's terminal is a placeholder that leaves a position open
3561
+ * (carrick#2714, `as { data: Item[] } | { error: unknown }`): the one type
3562
+ * stated for the value later in the function, on its success path
3563
+ * (`return json as { data: Item[] }`). A closed statement at the terminal
3564
+ * keeps the answer it had.
3565
+ */
3566
+ narrowedBodyStatement(terminal, func) {
3567
+ const placeholder = this.explicitTypeNodeFromAncestor(terminal);
3568
+ if (!placeholder || !this.leavesAPositionOpen(placeholder))
3569
+ return undefined;
3570
+ const stated = this.statedInFunction(terminal, func, {
3571
+ returned: true,
3572
+ parameters: false,
3573
+ skipFailurePaths: true,
3574
+ });
3575
+ return stated?.typeNode ? stated : undefined;
3576
+ }
3577
+ /**
3578
+ * The rule that verifies a call's result as a request library's response
3579
+ * object, or undefined (carrick#2714). The result is that object when its
3580
+ * awaited type is one a rule recognises. It is that object too when the
3581
+ * call states no type arguments and its declared result, with its free
3582
+ * type parameters at their defaults (`freeResultAtDefaults`), is a type a
3583
+ * rule recognises (`get<T, R = Response<T>>(): Promise<R>`).
3584
+ * The compiler then takes the parameter from where the value goes
3585
+ * (`return client.post(…)` in a function declared `Promise<void>` types the
3586
+ * call `Promise<void>`), and that is the caller's claim about the object,
3587
+ * not what the call returns.
3588
+ */
3589
+ transportRuleOfCall(callExpr, extractionConfig) {
3590
+ if (!extractionConfig || extractionConfig.rules.length === 0)
3591
+ return undefined;
3592
+ const awaited = this.unwrapPromiseType(callExpr.getType());
3593
+ const direct = this.ruleRecognising(awaited, callExpr, extractionConfig);
3594
+ if (direct)
3595
+ return { rule: direct };
3596
+ if (callExpr.getTypeArguments().length > 0)
3597
+ return undefined;
3598
+ const declared = this.freeResultAtDefaults(callExpr);
3599
+ const rule = declared ? this.ruleRecognising(declared, callExpr, extractionConfig) : undefined;
3600
+ return rule ? { rule } : undefined;
3601
+ }
3602
+ /**
3603
+ * The first rule that matches `type` as a wrapper whose body is a member
3604
+ * (`payloadPropertyPath`) the type has, whether or not it reads a payload
3605
+ * out of it. A
3606
+ * wrapper with no such member (the platform's `Response`) is read by its
3607
+ * body reads (`res.json()`), which the def-use walk follows.
3608
+ */
3609
+ ruleRecognising(type, node, config) {
3610
+ return config.rules.find((rule) => !!rule.payloadPropertyPath?.length &&
3611
+ !!type.getProperty(rule.payloadPropertyPath[0]) &&
3612
+ this.tryUnwrapWithRule(type, node, rule, config, 0).kind !== 'no-match');
3613
+ }
3614
+ /**
3615
+ * What a call's declared result is when its free type parameters take
3616
+ * their defaults, or undefined. A parameter is free when the declaration's
3617
+ * result mentions it and none of its parameters do, so only a type
3618
+ * argument, the default or the call's return position can fix it. The
3619
+ * result is read through `Promise`, a free parameter (its default), and a
3620
+ * conditional alias whose check type is a free parameter tested against
3621
+ * exactly that parameter's default (`R extends Default ? Response<T> : R`:
3622
+ * the true branch). Anything else is the type as written.
3623
+ */
3624
+ freeResultAtDefaults(callExpr) {
3625
+ const declaration = this.project
3626
+ .getTypeChecker()
3627
+ .getResolvedSignature(callExpr)
3628
+ ?.getDeclaration();
3629
+ if (!declaration ||
3630
+ !(Node.isMethodSignature(declaration) ||
3631
+ Node.isMethodDeclaration(declaration) ||
3632
+ Node.isFunctionDeclaration(declaration) ||
3633
+ Node.isCallSignatureDeclaration(declaration))) {
3634
+ return undefined;
3635
+ }
3636
+ const returnNode = declaration.getReturnTypeNode();
3637
+ if (!returnNode)
3638
+ return undefined;
3639
+ const referenced = (node) => new Set((node ? [node, ...node.getDescendants()] : [])
3640
+ .filter((n) => Node.isTypeReference(n))
3641
+ .map((n) => n.getTypeName().getText()));
3642
+ const inResult = referenced(returnNode);
3643
+ const inParameters = new Set(declaration.getParameters().flatMap((parameter) => [...referenced(parameter.getTypeNode())]));
3644
+ const free = new Map();
3645
+ for (const parameter of declaration.getTypeParameters()) {
3646
+ const name = parameter.getName();
3647
+ if (inResult.has(name) && !inParameters.has(name))
3648
+ free.set(name, parameter.getDefault());
3649
+ }
3650
+ if (free.size === 0)
3651
+ return undefined;
3652
+ const atDefaults = (node, depth) => {
3653
+ if (!node || depth > 4)
3654
+ return undefined;
3655
+ if (!Node.isTypeReference(node))
3656
+ return node.getType();
3657
+ const name = node.getTypeName().getText();
3658
+ if (free.has(name))
3659
+ return atDefaults(free.get(name), depth + 1);
3660
+ const args = node.getTypeArguments();
3661
+ if ((name === 'Promise' || name === 'PromiseLike') && args.length === 1) {
3662
+ return atDefaults(args[0], depth + 1);
3663
+ }
3664
+ const alias = node
3665
+ .getTypeName()
3666
+ .getSymbol()
3667
+ ?.getDeclarations()
3668
+ .find((d) => Node.isTypeAliasDeclaration(d));
3669
+ const conditional = alias && Node.isTypeAliasDeclaration(alias) ? alias.getTypeNode() : undefined;
3670
+ if (alias && Node.isTypeAliasDeclaration(alias) && conditional && Node.isConditionalTypeNode(conditional)) {
3671
+ const index = alias
3672
+ .getTypeParameters()
3673
+ .findIndex((parameter) => parameter.getName() === conditional.getCheckType().getText());
3674
+ const argument = index >= 0 ? args[index] : undefined;
3675
+ const tested = argument && Node.isTypeReference(argument) ? argument.getTypeName().getText() : undefined;
3676
+ const defaulted = tested !== undefined ? free.get(tested) : undefined;
3677
+ if (defaulted && conditional.getExtendsType().getText() === defaulted.getText()) {
3678
+ return atDefaults(conditional.getTrueType(), depth + 1);
3679
+ }
3680
+ }
3681
+ return node.getType();
3682
+ };
3683
+ return atDefaults(returnNode, 0);
3684
+ }
3685
+ /**
3686
+ * The call states the body itself: the rules read a payload out of its
3687
+ * result (`client.get<Widget>(url)`). An untyped payload is no payload to
3688
+ * the rules (they verify the object and read nothing), and a result its
3689
+ * return position chose is no response object to them.
3690
+ */
3691
+ callStatesTypedPayload(callExpr, extractionConfig) {
3692
+ const unwrap = this.unwrapTypeWithConfig(this.unwrapPromiseType(callExpr.getType()), callExpr, extractionConfig);
3693
+ return unwrap.wasUnwrapped && !unwrap.verifiedMachinery && !!unwrap.payloadType;
3694
+ }
3695
+ /**
3696
+ * The body of a call that returns a response object (carrick#2714): the
3697
+ * one type the source states, in the function the call is made in, for the
3698
+ * reads of the object's payload member (`res.data as Widget`, `const w:
3699
+ * Widget = res.data`, `return res.data` from a function that declares its
3700
+ * return type), through a binding or in place. Where no read states one,
3701
+ * or two state different types, or the object is handed on whole, the
3702
+ * site states no response contract, and the abstain is decided so the
3703
+ * capture does not re-read the call and publish the object.
3704
+ */
3705
+ transportCallBody(request, callExpr, func, rule, where) {
3706
+ const reads = this.payloadMemberReads(callExpr, func, rule.payloadPropertyPath ?? []);
3707
+ const statements = reads
3708
+ .map((read) => this.statedInFunction(read, func, { returned: true, parameters: true, skipFailurePaths: true }))
3709
+ .filter((stated) => stated !== null);
3710
+ const texts = new Set(statements.map((stated) => stated.text));
3711
+ if (texts.size !== 1) {
3712
+ this.log(`Call result at ${where} is a response object around the body, and the source states ` +
3713
+ `${texts.size === 0 ? 'no type' : `${texts.size} different types`} for the body it reads; ` +
3714
+ 'this site states no response contract');
3715
+ return this.transportAbstain(request, callExpr, "this call returns the client's response object around the body, and the source " +
3716
+ 'states no single type for the body it reads from it, so this site states no ' +
3717
+ 'response contract');
3718
+ }
3719
+ const [stated] = statements;
3720
+ this.log(`Call result at ${where} is a response object; the source states its body (${stated.how})`);
3721
+ const anchor = stated.typeNode
3722
+ ? this.unwrapArrayLevels(this.unwrapPromiseType(stated.typeNode.getType()))
3723
+ : undefined;
3724
+ const inferred = this.createInferredType(request, stated.text, true, this.getNodeLocation(stated.at), undefined, anchor ? this.primaryTypeSymbol(anchor.element) : undefined, anchor?.depth, anchor ? this.primaryTypeSymbolSource(anchor.element) : undefined);
3725
+ if (stated.typeNode) {
3726
+ inferred.stated_body = this.statedRoot(stated.typeNode);
3727
+ }
3728
+ return inferred;
3729
+ }
3730
+ /**
3731
+ * The reads of a response object's payload member (`path`, e.g. `data`)
3732
+ * that take it off the call's value: in place (`(await call).data`),
3733
+ * through the name the value is bound to (`res.data`), or as a name
3734
+ * destructured out of it (`const { data } = await call`).
3735
+ */
3736
+ payloadMemberReads(callExpr, func, path) {
3737
+ if (path.length === 0)
3738
+ return [];
3739
+ const follow = (start) => {
3740
+ let node = start;
3741
+ for (const segment of path) {
3742
+ const parent = node.getParent();
3743
+ if (!parent ||
3744
+ !Node.isPropertyAccessExpression(parent) ||
3745
+ parent.getExpression() !== node ||
3746
+ parent.getName() !== segment) {
3747
+ return undefined;
3748
+ }
3749
+ node = parent;
3750
+ }
3751
+ return node;
3752
+ };
3753
+ let value = callExpr;
3754
+ for (;;) {
3755
+ const parent = value.getParent();
3756
+ if (parent &&
3757
+ (Node.isParenthesizedExpression(parent) ||
3758
+ Node.isAwaitExpression(parent) ||
3759
+ Node.isNonNullExpression(parent)) &&
3760
+ parent.getExpression() === value) {
3761
+ value = parent;
3762
+ continue;
3763
+ }
3764
+ break;
3765
+ }
3766
+ const inPlace = follow(value);
3767
+ if (inPlace)
3768
+ return [inPlace];
3769
+ const declaration = value.getParent();
3770
+ if (!declaration || !Node.isVariableDeclaration(declaration) || declaration.getInitializer() !== value) {
3771
+ return [];
3772
+ }
3773
+ const name = declaration.getNameNode();
3774
+ if (Node.isObjectBindingPattern(name)) {
3775
+ if (path.length !== 1)
3776
+ return [];
3777
+ return name
3778
+ .getElements()
3779
+ .filter((element) => !element.getDotDotDotToken() &&
3780
+ (element.getPropertyNameNode()?.getText() ?? element.getName()) === path[0] &&
3781
+ Node.isIdentifier(element.getNameNode()))
3782
+ .map((element) => element.getNameNode());
3783
+ }
3784
+ if (!Node.isIdentifier(name))
3785
+ return [];
3786
+ const symbol = name.getSymbol()?.compilerSymbol;
3787
+ if (!symbol)
3788
+ return [];
3789
+ return func
3790
+ .getDescendantsOfKind(SyntaxKind.Identifier)
3791
+ .filter((id) => id !== name && id.getSymbol()?.compilerSymbol === symbol)
3792
+ .map(follow)
3793
+ .filter((read) => read !== undefined);
3794
+ }
3406
3795
  /**
3407
3796
  * The decided abstain of a call whose result carries transport the
3408
3797
  * service's wrapper rules verify and read no payload out of (carrick#1841,
@@ -6305,14 +6694,17 @@ export class TypeInferrer {
6305
6694
  return undefined;
6306
6695
  }
6307
6696
  /**
6308
- * The handler function referenced by a route-registration call: the first
6697
+ * The handler function referenced by a route-registration call: the LAST
6309
6698
  * argument that is an inline function, or an identifier bound to a function
6310
- * (typically the 2nd+ arg — the path literal is not function-valued, so it is
6311
- * skipped naturally). No callee-name check: a call whose argument resolves to
6312
- * a function is structurally a registration regardless of the framework.
6699
+ * (carrick#2472 P3-2b). The functions before it are middleware
6700
+ * (`route(path).post(auth, audit, handler)`), which runs first and hands on;
6701
+ * reading the first function read a middleware as the route. The path
6702
+ * literal is not function-valued, so it is skipped naturally. No callee-name
6703
+ * check: a call whose argument resolves to a function is structurally a
6704
+ * registration regardless of the framework.
6313
6705
  */
6314
6706
  handlerFromRegistrationCall(call) {
6315
- for (const arg of call.getArguments()) {
6707
+ for (const arg of [...call.getArguments()].reverse()) {
6316
6708
  const handler = this.asHandlerFunction(arg);
6317
6709
  if (handler) {
6318
6710
  return handler;
@@ -6351,15 +6743,17 @@ export class TypeInferrer {
6351
6743
  * declaration (via the compiler's definition nodes) and returned when that
6352
6744
  * declaration is — or initializes to — a function. Anything else (a path
6353
6745
  * literal, an object, a non-function binding) yields undefined.
6746
+ * `followWrapperCalls` false reads a binding to a wrapper call as no
6747
+ * function, so a name the wrapper walk meets is never walked again.
6354
6748
  */
6355
- asHandlerFunction(node) {
6749
+ asHandlerFunction(node, followWrapperCalls = true) {
6356
6750
  const expr = this.unwrapExpressionNode(node);
6357
6751
  if (Node.isArrowFunction(expr) || Node.isFunctionExpression(expr)) {
6358
6752
  return expr;
6359
6753
  }
6360
6754
  if (Node.isIdentifier(expr)) {
6361
6755
  for (const def of expr.getDefinitionNodes()) {
6362
- const func = this.functionFromDeclaration(def);
6756
+ const func = this.functionFromDeclaration(def, followWrapperCalls);
6363
6757
  if (func) {
6364
6758
  return func;
6365
6759
  }
@@ -6371,10 +6765,13 @@ export class TypeInferrer {
6371
6765
  * Extract a `FunctionLike` from a declaration node the compiler resolved an
6372
6766
  * identifier to: a function declaration is itself the function; a variable
6373
6767
  * declaration / binding whose initializer is an inline function yields that
6374
- * function (`const h = async () => { … }`). Import/re-export shims are walked
6375
- * by ts-morph's `getDefinitionNodes`, so no manual import chasing is needed.
6768
+ * function (`const h = async () => { … }`), and one whose initializer is a
6769
+ * wrapper call yields the function the call wraps
6770
+ * (`const h = wrap<Contract>(async (req, res) => { … })`, carrick#2472
6771
+ * P3-2b; `functionWrappedBy`). Import/re-export shims are walked by
6772
+ * ts-morph's `getDefinitionNodes`, so no manual import chasing is needed.
6376
6773
  */
6377
- functionFromDeclaration(decl) {
6774
+ functionFromDeclaration(decl, followWrapperCalls = true) {
6378
6775
  if (Node.isFunctionDeclaration(decl)) {
6379
6776
  return decl;
6380
6777
  }
@@ -6399,10 +6796,43 @@ export class TypeInferrer {
6399
6796
  if (Node.isArrowFunction(inner) || Node.isFunctionExpression(inner)) {
6400
6797
  return inner;
6401
6798
  }
6799
+ if (followWrapperCalls && Node.isCallExpression(inner)) {
6800
+ return this.functionWrappedBy(inner, 0);
6801
+ }
6402
6802
  }
6403
6803
  }
6404
6804
  return undefined;
6405
6805
  }
6806
+ /**
6807
+ * The function a call wraps, for a name bound to that call and used as a
6808
+ * handler (carrick#2472 P3-2b): the call's one argument that is a function
6809
+ * literal, a name bound to one, or another such call, a few calls deep.
6810
+ *
6811
+ * Two conditions keep this to wrappers. The call's own value has to be
6812
+ * callable, because the name is used as a function: `rows.map((row) => …)`
6813
+ * is handed a function and yields a list. And exactly one argument may be a
6814
+ * function: with two there is no saying which of them is wrapped.
6815
+ */
6816
+ functionWrappedBy(call, depth) {
6817
+ if (depth >= WRAPPER_CALL_MAX_DEPTH)
6818
+ return undefined;
6819
+ if (!this.isCallableType(call.getType()))
6820
+ return undefined;
6821
+ const wrapped = [];
6822
+ for (const arg of call.getArguments()) {
6823
+ const value = this.unwrapExpressionNode(arg);
6824
+ const func = Node.isArrowFunction(value) || Node.isFunctionExpression(value)
6825
+ ? value
6826
+ : Node.isCallExpression(value)
6827
+ ? this.functionWrappedBy(value, depth + 1)
6828
+ : Node.isIdentifier(value)
6829
+ ? this.asHandlerFunction(value, false)
6830
+ : undefined;
6831
+ if (func)
6832
+ wrapped.push(func);
6833
+ }
6834
+ return wrapped.length === 1 ? wrapped[0] : undefined;
6835
+ }
6406
6836
  /**
6407
6837
  * True when `handler` reads nothing from the incoming message (carrick#827):
6408
6838
  * every use of one of its parameters, named or destructured, is the receiver
@@ -7045,6 +7475,15 @@ export class TypeInferrer {
7045
7475
  * below reads it.
7046
7476
  */
7047
7477
  bodyBuiltByCallee(node) {
7478
+ const read = this.calleeBodyRead(node, 0);
7479
+ return read.kind === 'passes' ? { kind: 'none' } : read;
7480
+ }
7481
+ /**
7482
+ * `bodyBuiltByCallee`, telling apart a callee that sends the argument
7483
+ * unchanged (`passes`) from one that states nothing about it (`none`).
7484
+ * `depth` counts the forwarded locals followed (carrick#2671).
7485
+ */
7486
+ calleeBodyRead(node, depth) {
7048
7487
  const own = node.getType();
7049
7488
  if (own.isAny() || own.isUnknown())
7050
7489
  return { kind: 'none' };
@@ -7107,6 +7546,15 @@ export class TypeInferrer {
7107
7546
  built.push(value);
7108
7547
  continue;
7109
7548
  }
7549
+ // carrick#2671: the use builds a local (`const payload = { ...input,
7550
+ // code: norm(input.code) }`) the function then sends unchanged, itself
7551
+ // or through a helper that passes it on as the body. The body is that
7552
+ // local.
7553
+ const forwarded = this.forwardedLocalHolding(use, body, depth);
7554
+ if (forwarded) {
7555
+ built.push(forwarded);
7556
+ continue;
7557
+ }
7110
7558
  const ancestors = use.getAncestors();
7111
7559
  const insideLiteral = ancestors
7112
7560
  .slice(0, ancestors.indexOf(body))
@@ -7119,7 +7567,7 @@ export class TypeInferrer {
7119
7567
  }
7120
7568
  }
7121
7569
  if (built.length === 0)
7122
- return { kind: 'none' };
7570
+ return passedThrough ? { kind: 'passes' } : { kind: 'none' };
7123
7571
  if (passedThrough) {
7124
7572
  return { kind: 'abstain', why: `the called function both sends the argument and builds a body from it (${from})` };
7125
7573
  }
@@ -7170,6 +7618,78 @@ export class TypeInferrer {
7170
7618
  }
7171
7619
  return undefined;
7172
7620
  }
7621
+ /**
7622
+ * carrick#2671: the `const` local whose initializer holds `use`, inside
7623
+ * `scope`, when `scope` sends that local unchanged as the body: whole in a
7624
+ * body slot, or as an argument into a function that passes it on as its
7625
+ * body. The local's name, so its declared type is read. Undefined when the
7626
+ * local is never sent, when a further body is built from it, or when a
7627
+ * function lies between `use` and the declaration.
7628
+ */
7629
+ forwardedLocalHolding(use, scope, depth) {
7630
+ if (depth >= 2)
7631
+ return undefined;
7632
+ let current = use;
7633
+ let declaration;
7634
+ while (current !== scope) {
7635
+ const parent = current.getParent();
7636
+ if (!parent)
7637
+ return undefined;
7638
+ if (Node.isArrowFunction(parent) ||
7639
+ Node.isFunctionExpression(parent) ||
7640
+ Node.isFunctionDeclaration(parent) ||
7641
+ Node.isMethodDeclaration(parent)) {
7642
+ return undefined;
7643
+ }
7644
+ if (Node.isVariableDeclaration(parent) && parent.getInitializer() === current) {
7645
+ declaration = parent;
7646
+ break;
7647
+ }
7648
+ current = parent;
7649
+ }
7650
+ if (!declaration)
7651
+ return undefined;
7652
+ if (declaration.getVariableStatement()?.getDeclarationKind() !== VariableDeclarationKind.Const) {
7653
+ return undefined;
7654
+ }
7655
+ const name = declaration.getNameNode();
7656
+ if (!Node.isIdentifier(name))
7657
+ return undefined;
7658
+ const symbol = name.getSymbol()?.compilerSymbol;
7659
+ if (!symbol)
7660
+ return undefined;
7661
+ const checker = this.project.getTypeChecker().compilerObject;
7662
+ let sent = false;
7663
+ for (const ref of scope.getDescendantsOfKind(SyntaxKind.Identifier)) {
7664
+ if (ref === name)
7665
+ continue;
7666
+ const parent = ref.getParent();
7667
+ const refSymbol = parent && Node.isShorthandPropertyAssignment(parent)
7668
+ ? checker.getShorthandAssignmentValueSymbol(parent.compilerNode)
7669
+ : ref.getSymbol()?.compilerSymbol;
7670
+ if (refSymbol !== symbol)
7671
+ continue;
7672
+ if (this.inSendNothingTest(ref, scope))
7673
+ continue;
7674
+ const value = this.bodyValueAround(ref, scope);
7675
+ if (value === ref) {
7676
+ sent = true;
7677
+ continue;
7678
+ }
7679
+ if (value)
7680
+ return undefined;
7681
+ if (parent && Node.isCallExpression(parent) && parent.getArguments().includes(ref)) {
7682
+ const onward = this.calleeBodyRead(ref, depth + 1);
7683
+ if (onward.kind === 'passes') {
7684
+ sent = true;
7685
+ continue;
7686
+ }
7687
+ if (onward.kind !== 'none')
7688
+ return undefined;
7689
+ }
7690
+ }
7691
+ return sent ? name : undefined;
7692
+ }
7173
7693
  /** `value` is `use` behind what `sentValue` sees through. */
7174
7694
  isSameValue(value, use) {
7175
7695
  return this.sentValue(value) === use;
@@ -7442,6 +7962,15 @@ export class TypeInferrer {
7442
7962
  * carrying the provenance the reading recorded.
7443
7963
  */
7444
7964
  declaredRequestInferredType(request, contract, location) {
7965
+ if (contract.noRequestBody !== undefined) {
7966
+ this.log(`Route at ${request.file_path}:${request.line_number} states no request body: ${contract.noRequestBody}`);
7967
+ return this.decidedAbstain(request, location, {
7968
+ path: '',
7969
+ kind: 'unknown',
7970
+ reason: 'no_request_body',
7971
+ detail: contract.noRequestBody,
7972
+ });
7973
+ }
7445
7974
  const inferred = this.createInferredType(request, contract.text, true, location, undefined, undefined);
7446
7975
  if (contract.provenance && contract.provenance.length > 0) {
7447
7976
  inferred.any_provenance = contract.provenance;
@@ -7460,12 +7989,74 @@ export class TypeInferrer {
7460
7989
  * own locator authoritative when a route declares nothing.
7461
7990
  */
7462
7991
  declaredRequestContract(registration, handler) {
7992
+ // carrick#2674: the body member a verified label names, read at the
7993
+ // handler's parameter, comes first. Where no label answers, the old rule
7994
+ // that reads a member by its name stands.
7995
+ const stated = this.statedParams.request(handler, registration);
7996
+ if (stated?.kind === 'body') {
7997
+ const text = this.structuralTextFromType(stated.type, stated.at);
7998
+ if (text) {
7999
+ return { text };
8000
+ }
8001
+ }
7463
8002
  const annotated = this.requestBodyFromHandlerParams(handler);
7464
8003
  if (annotated) {
7465
8004
  return { text: annotated };
7466
8005
  }
7467
- return (this.routeSchemaContract(registration, 'body') ??
7468
- this.validatedBodyContract(registration));
8006
+ const schema = this.routeSchemaContract(registration, 'body') ??
8007
+ this.validatedBodyContract(registration);
8008
+ if (schema) {
8009
+ return schema;
8010
+ }
8011
+ return stated?.kind === 'no_body' ? { text: 'unknown', noRequestBody: stated.detail } : null;
8012
+ }
8013
+ /** The stated-parameter reader over `labels`, judging files and statuses as this inferrer does. */
8014
+ statedParamsFor(labels) {
8015
+ return new StatedParams(labels, (file) => {
8016
+ const sourceFile = file.getSourceFile();
8017
+ if (sourceFile.isDeclarationFile())
8018
+ return false;
8019
+ return !isExternalOrigin(this.project.getProgram().compilerObject, sourceFile.compilerNode, this.repoRoot, externalImportsOf(this.project));
8020
+ }, (call) => this.chainStatus(call));
8021
+ }
8022
+ /**
8023
+ * carrick#2674: where today's reading of a route's response is empty
8024
+ * (nothing, `void`, or an `unknown` the inferrer did not decide), the reply
8025
+ * its handler's parameter states, cut to its success variants. A typed
8026
+ * reading, or a decided abstain, stands as it is.
8027
+ */
8028
+ withStatedReply(sourceFile, request, today) {
8029
+ if (!this.statedParams.readsReplies)
8030
+ return today;
8031
+ if (today && !this.isEmptyResponseReading(today))
8032
+ return today;
8033
+ const handler = this.registrationAtLine(sourceFile, request.line_number)?.handler ??
8034
+ this.exportedFunctionOpeningOn(sourceFile, request.line_number);
8035
+ if (!handler)
8036
+ return today;
8037
+ const stated = this.statedParams.reply(handler);
8038
+ if (!stated)
8039
+ return today;
8040
+ const texts = [];
8041
+ for (const variant of stated.variants) {
8042
+ const text = this.structuralTextFromType(variant, stated.at);
8043
+ // A variant holding `any` or `unknown` is one the check refuses as an
8044
+ // anchor (`usable_inferred_text`): publishing it would fall back to the
8045
+ // raw anchor under an explicit flag, so today's reading stands.
8046
+ if (!text || printedTypeHoldsTopType(text))
8047
+ return today;
8048
+ texts.push(text);
8049
+ }
8050
+ this.log(`Route at ${request.file_path}:${request.line_number}: its handler's reply parameter states the body`);
8051
+ return this.createInferredType(request, texts.length === 1 ? texts[0] : texts.map((text) => `(${text})`).join(' | '), true, this.getNodeLocation(handler));
8052
+ }
8053
+ /** Nothing read: `void`, or an `unknown` with no decided reason at its root. */
8054
+ isEmptyResponseReading(inferred) {
8055
+ if (inferred.type_string === 'void')
8056
+ return true;
8057
+ if (inferred.type_string !== 'unknown')
8058
+ return false;
8059
+ return !(inferred.any_provenance ?? []).some((provenance) => provenance.path === '' && DECIDED_ABSTAIN_REASONS.has(provenance.reason));
7469
8060
  }
7470
8061
  /**
7471
8062
  * Anchor (a): the request contract declared on the handler's own signature.
@@ -7678,7 +8269,7 @@ export class TypeInferrer {
7678
8269
  return;
7679
8270
  const text = this.structuralTextFromTypeNode(typeNode);
7680
8271
  if (text)
7681
- found.push({ rank, text, at, how });
8272
+ found.push({ rank, text, at, how, typeNode });
7682
8273
  };
7683
8274
  for (let i = 0; i < positions.length; i++) {
7684
8275
  const position = positions[i];
@@ -7765,7 +8356,7 @@ export class TypeInferrer {
7765
8356
  return null;
7766
8357
  }
7767
8358
  const [first] = decided;
7768
- return { text: first.text, provenance: first.provenance, at: first.at, how: first.how };
8359
+ return { text: first.text, provenance: first.provenance, at: first.at, how: first.how, typeNode: first.typeNode };
7769
8360
  }
7770
8361
  /**
7771
8362
  * What a call that takes the value at argument `index` states for it:
@@ -8990,3 +9581,12 @@ export class TypeInferrer {
8990
9581
  console.error(`[sidecar:type-inferrer:error] ${message}`);
8991
9582
  }
8992
9583
  }
9584
+ /** The node a `SavedSpan` was taken from, in the file as it reads now. */
9585
+ function nodeAtSpan(span) {
9586
+ let node = span.file.getDescendantAtPos(span.start);
9587
+ while (node &&
9588
+ !(node.getStart() === span.start && node.getEnd() === span.end && node.getKind() === span.kind)) {
9589
+ node = node.getParent();
9590
+ }
9591
+ return node;
9592
+ }