carrick 0.3.127 → 0.3.129

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';
@@ -187,6 +188,18 @@ const WRAPPER_CALL_MAX_DEPTH = 3;
187
188
  * the branch an error path, whose shape is not the endpoint's contract.
188
189
  */
189
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
+ ]);
190
203
  /** The provenance sentence for each reason a branch was not read. */
191
204
  const SERIALISED_BODY_UNREAD_DETAIL = {
192
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.',
@@ -448,13 +461,16 @@ export class TypeInferrer {
448
461
  * @param onRequestDone - Called once per request, as the batch is done with it
449
462
  * @returns InferResult with inferred types or errors
450
463
  */
451
- infer(requests, extractionConfig, onRequestDone, modelSymbols) {
464
+ infer(requests, extractionConfig, onRequestDone, modelSymbols, labels) {
452
465
  const started = performance.now();
453
466
  this.modelSymbols = new Map((modelSymbols ?? []).map((symbol) => [symbol.alias, symbol]));
467
+ this.statedParams = this.statedParamsFor(labels ?? { sends: new Map(), bodies: new Set() });
454
468
  const inferredTypes = [];
455
469
  const errors = [];
456
470
  /** Response inferences the unwidened reading re-reads (carrick#1516). */
457
471
  const responses = [];
472
+ /** Request-body inferences it re-reads after them (carrick#2011). */
473
+ const bodies = [];
458
474
  /** How long each request took, in the order they were done (carrick#1985). */
459
475
  const timings = [];
460
476
  for (const request of requests) {
@@ -491,6 +507,9 @@ export class TypeInferrer {
491
507
  if (request.infer_kind === 'response_body' || request.infer_kind === 'function_return') {
492
508
  responses.push({ request, result });
493
509
  }
510
+ else if (request.infer_kind === 'request_body') {
511
+ bodies.push({ request, result });
512
+ }
494
513
  }
495
514
  else {
496
515
  errors.push(`Could not infer type at ${request.file_path}:${loc} (${request.infer_kind})`);
@@ -521,10 +540,47 @@ export class TypeInferrer {
521
540
  }
522
541
  try {
523
542
  const deadline = Math.min(performance.now() + this.unwidenedBudgetMs, started + UNWIDENED_LATEST_MS);
524
- 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
+ }
525
582
  }
526
583
  catch (err) {
527
- // The reading only ever adds a field; it never costs an answer.
528
584
  this.logError(`Unwidened reading failed: ${err instanceof Error ? err.message : String(err)}`);
529
585
  }
530
586
  return {
@@ -536,6 +592,12 @@ export class TypeInferrer {
536
592
  }
537
593
  /** The types the model named, by the alias of the row each is for (carrick#2111). */
538
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() });
539
601
  /**
540
602
  * carrick#2111: where the model named a type for the response this request
541
603
  * reads, record on `inferred` how that type relates to the compiler's type
@@ -620,6 +682,10 @@ export class TypeInferrer {
620
682
  * really returns beside the published one (`unwidened_type_string`). See
621
683
  * `unwidened.ts` for what is marked and why the reading is sound.
622
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
+ *
623
689
  * One rewrite for the whole batch: every file on any request's path is
624
690
  * rewritten once, type-checked before and after, the requests re-inferred,
625
691
  * and every file restored before returning, so the project other requests
@@ -631,7 +697,7 @@ export class TypeInferrer {
631
697
  * reading (a declared return, nothing to mark) or lost it for a reason of
632
698
  * its own (a marking that added a diagnostic) is never marked.
633
699
  */
634
- addUnwidenedReadings(responses, extractionConfig, deadline) {
700
+ addUnwidenedReadings(responses, extractionConfig, deadline, read) {
635
701
  const outOfTime = (stage, unread) => {
636
702
  if (performance.now() <= deadline)
637
703
  return false;
@@ -646,17 +712,19 @@ export class TypeInferrer {
646
712
  const checkedFiles = new Set();
647
713
  // Every position is read before the first rewrite: a rewrite forgets
648
714
  // every node of its file.
649
- for (const { request, result } of responses) {
715
+ for (const { request, result, readSpan } of responses) {
650
716
  // A declared return type is the contract; nothing was widened.
651
717
  if (result.is_explicit)
652
718
  continue;
653
- const read = this.readNodes.get(result.source_location);
654
- if (!read)
719
+ const readNode = readSpan
720
+ ? nodeAtSpan(readSpan)
721
+ : this.readNodes.get(result.source_location);
722
+ if (!readNode)
655
723
  continue;
656
- const functions = pathOf(read);
724
+ const functions = pathOf(readNode);
657
725
  let marked = 0;
658
726
  for (const fn of functions) {
659
- const insertions = literalInsertions(fn);
727
+ const insertions = literalInsertions(fn, read === 'request_body' ? readNode : undefined);
660
728
  marked += insertions.length;
661
729
  if (insertions.length === 0)
662
730
  continue;
@@ -685,7 +753,7 @@ export class TypeInferrer {
685
753
  plans.push({
686
754
  request,
687
755
  result,
688
- read: { file: read.getSourceFile(), start: read.getStart(), end: read.getEnd() },
756
+ read: { file: readNode.getSourceFile(), start: readNode.getStart(), end: readNode.getEnd() },
689
757
  path,
690
758
  locatedByText,
691
759
  });
@@ -807,7 +875,7 @@ export class TypeInferrer {
807
875
  case 'function_return':
808
876
  return this.inferFunctionReturn(sourceFile, request, extractionConfig);
809
877
  case 'response_body':
810
- return this.inferResponseBody(sourceFile, request, extractionConfig);
878
+ return this.withStatedReply(sourceFile, request, this.inferResponseBody(sourceFile, request, extractionConfig));
811
879
  case 'call_result':
812
880
  return this.inferCallResult(sourceFile, request, extractionConfig);
813
881
  case 'variable':
@@ -1909,6 +1977,16 @@ export class TypeInferrer {
1909
1977
  typeString = explicitType;
1910
1978
  isExplicit = true;
1911
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
+ }
1912
1990
  // carrick#1843: a rule can now match a thenable or a carrier by its alias,
1913
1991
  // so on a result that is a carrier (`Task<Outcome<Reply<T>, E>>`) the
1914
1992
  // rules reach what the carrier holds before the carrier read below does.
@@ -1920,6 +1998,17 @@ export class TypeInferrer {
1920
1998
  const callArgs = this.callResultArguments(callExpr);
1921
1999
  const resultIsCarrier = !explicitType &&
1922
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
+ }
1923
2012
  // carrick#1877: the terminal holds a thenable the names do not peel, a
1924
2013
  // subclass of `Promise` or a class with a `then` of its own. What a
1925
2014
  // caller receives is what `await` yields for it, as it is for the
@@ -2133,7 +2222,9 @@ export class TypeInferrer {
2133
2222
  const leftCarrier = (callUnwrap.wasUnwrapped || readByProtocol) &&
2134
2223
  !!callAfterRules &&
2135
2224
  this.resultCarrierArguments(callAfterRules, callExpr, callArgs) !== undefined;
2136
- const anchorSource = carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
2225
+ const anchorSource = narrowed?.typeNode
2226
+ ? narrowed.typeNode.getType()
2227
+ : carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
2137
2228
  let anchor = anchorSource
2138
2229
  ? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
2139
2230
  : undefined;
@@ -2182,7 +2273,11 @@ export class TypeInferrer {
2182
2273
  // carrick#1749: say when the text is the source's own statement of the
2183
2274
  // body, and what that statement is rooted at, so the scanner can tell a
2184
2275
  // model symbol that names the body from one that names a part of it.
2185
- 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;
2186
2281
  if (statedBody) {
2187
2282
  inferred.stated_body = statedBody;
2188
2283
  }
@@ -3460,6 +3555,243 @@ export class TypeInferrer {
3460
3555
  }
3461
3556
  return common;
3462
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
+ }
3463
3795
  /**
3464
3796
  * The decided abstain of a call whose result carries transport the
3465
3797
  * service's wrapper rules verify and read no payload out of (carrick#1841,
@@ -7630,6 +7962,15 @@ export class TypeInferrer {
7630
7962
  * carrying the provenance the reading recorded.
7631
7963
  */
7632
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
+ }
7633
7974
  const inferred = this.createInferredType(request, contract.text, true, location, undefined, undefined);
7634
7975
  if (contract.provenance && contract.provenance.length > 0) {
7635
7976
  inferred.any_provenance = contract.provenance;
@@ -7648,12 +7989,74 @@ export class TypeInferrer {
7648
7989
  * own locator authoritative when a route declares nothing.
7649
7990
  */
7650
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
+ }
7651
8002
  const annotated = this.requestBodyFromHandlerParams(handler);
7652
8003
  if (annotated) {
7653
8004
  return { text: annotated };
7654
8005
  }
7655
- return (this.routeSchemaContract(registration, 'body') ??
7656
- 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));
7657
8060
  }
7658
8061
  /**
7659
8062
  * Anchor (a): the request contract declared on the handler's own signature.
@@ -7866,7 +8269,7 @@ export class TypeInferrer {
7866
8269
  return;
7867
8270
  const text = this.structuralTextFromTypeNode(typeNode);
7868
8271
  if (text)
7869
- found.push({ rank, text, at, how });
8272
+ found.push({ rank, text, at, how, typeNode });
7870
8273
  };
7871
8274
  for (let i = 0; i < positions.length; i++) {
7872
8275
  const position = positions[i];
@@ -7953,7 +8356,7 @@ export class TypeInferrer {
7953
8356
  return null;
7954
8357
  }
7955
8358
  const [first] = decided;
7956
- 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 };
7957
8360
  }
7958
8361
  /**
7959
8362
  * What a call that takes the value at argument `index` states for it:
@@ -9178,3 +9581,12 @@ export class TypeInferrer {
9178
9581
  console.error(`[sidecar:type-inferrer:error] ${message}`);
9179
9582
  }
9180
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
+ }