carrick 0.3.114 → 0.3.116

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.
@@ -21,6 +21,8 @@ import * as path from 'node:path';
21
21
  import { Node, SyntaxKind, VariableDeclarationKind, ts, } from 'ts-morph';
22
22
  import { typeToStringAsDeclared } from './print-type.js';
23
23
  import { validateInferRequestItem } from './validators.js';
24
+ import { declarationSite } from './bundler.js';
25
+ import { printedTypeHoldsTopType, relateSymbolToRead } from './symbol-relation.js';
24
26
  import { notePrintedType, PrintedTypes } from './printed-names.js';
25
27
  import { externalImportsOf, isExternalOrigin } from './origin.js';
26
28
  import { reachedOnlyOnFailure } from './failure-path.js';
@@ -28,7 +30,7 @@ import { readResponseModes } from './response-modes.js';
28
30
  import { functionAtLine } from './function-line-index.js';
29
31
  import { endLineOf, startLineOf } from './line-index.js';
30
32
  import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
31
- import { classifyStatusCodes, constituents, httpStatus, statesNoBody, statusKeyCode, statusTableOf, successEntries, } from './capture/index.js';
33
+ import { classifyStatusCodes, constituents, httpStatus, statesNoBody, statesNoShape, statusKeyCode, statusTableOf, successEntries, } from './capture/index.js';
32
34
  import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
33
35
  import { expandTypeStructural, jsonWireType, } from './type-structural-expander.js';
34
36
  /**
@@ -240,6 +242,12 @@ const BODY_MEMBER_NAMES = new Set(['data', 'body', 'json']);
240
242
  * JSON, or as raw text (carrick#1842).
241
243
  */
242
244
  const WHOLE_BODY_READS = new Set(['json', 'text']);
245
+ /**
246
+ * The other zero-argument reads the platform `Response` takes its whole body
247
+ * out with, in a form that is neither JSON nor text. The expression that
248
+ * holds one holds the body the call answered with (carrick#2112).
249
+ */
250
+ const BINARY_BODY_READS = new Set(['arrayBuffer', 'blob', 'bytes', 'formData']);
243
251
  /** The body format a caller names to read a response as raw text (carrick#1842). */
244
252
  const RAW_TEXT_FORMAT = 'text';
245
253
  /**
@@ -415,8 +423,9 @@ export class TypeInferrer {
415
423
  * @param onRequestDone - Called once per request, as the batch is done with it
416
424
  * @returns InferResult with inferred types or errors
417
425
  */
418
- infer(requests, extractionConfig, onRequestDone) {
426
+ infer(requests, extractionConfig, onRequestDone, modelSymbols) {
419
427
  const started = performance.now();
428
+ this.modelSymbols = new Map((modelSymbols ?? []).map((symbol) => [symbol.alias, symbol]));
420
429
  const inferredTypes = [];
421
430
  const errors = [];
422
431
  /** Response inferences the unwidened reading re-reads (carrick#1516). */
@@ -500,6 +509,74 @@ export class TypeInferrer {
500
509
  errors: errors.length > 0 ? errors : undefined,
501
510
  };
502
511
  }
512
+ /** The types the model named, by the alias of the row each is for (carrick#2111). */
513
+ modelSymbols = new Map();
514
+ /**
515
+ * carrick#2111: where the model named a type for the response this request
516
+ * reads, record on `inferred` how that type relates to the compiler's type
517
+ * of what the handler sends (`read`, one type per surviving branch). The
518
+ * driver keeps, re-depths or drops the model's symbol on the answer.
519
+ *
520
+ * Nothing is recorded, and the symbol stands, where there is nothing to
521
+ * compare: the symbol does not resolve in this program, or the published
522
+ * read holds `any` or `unknown` somewhere.
523
+ */
524
+ relateModelSymbol(request, inferred, read) {
525
+ if (request.infer_kind !== 'response_body' && request.infer_kind !== 'function_return')
526
+ return;
527
+ const named = this.modelSymbols.get(inferred.alias);
528
+ if (!named)
529
+ return;
530
+ const types = read.filter((type) => type !== undefined);
531
+ if (types.length !== read.length || types.length === 0)
532
+ return;
533
+ if (printedTypeHoldsTopType(inferred.type_string))
534
+ return;
535
+ try {
536
+ const symbolType = this.modelSymbolType(named);
537
+ if (!symbolType)
538
+ return;
539
+ const answer = relateSymbolToRead(this.project.getTypeChecker().compilerObject, symbolType.compilerType, types.map((type) => type.compilerType));
540
+ if (answer)
541
+ inferred.model_symbol_relation = answer;
542
+ }
543
+ catch (err) {
544
+ // The relation only ever adds an answer; it never costs one.
545
+ this.logError(`Model symbol relation failed for ${inferred.alias}: ${err instanceof Error ? err.message : String(err)}`);
546
+ }
547
+ }
548
+ /**
549
+ * `func` is the function the row's line names: it opens on that line (a
550
+ * method under its decorators, an exported handler), or it is the handler
551
+ * of the registration that opens there (`app.post<…>(\n '/path',\n async
552
+ * (request) => { … })`, or a named handler declared elsewhere).
553
+ */
554
+ isTheRowsOwnFunction(request, func) {
555
+ if (startLineOf(func) === request.line_number)
556
+ return true;
557
+ return this.registrationAtLine(func.getSourceFile(), request.line_number)?.handler === func;
558
+ }
559
+ /**
560
+ * The declared type of a symbol the model named, found as the bundle finds
561
+ * it (through a barrel's re-exports), in a file this program already
562
+ * holds. A name that declares a value and no type resolves to nothing.
563
+ */
564
+ modelSymbolType(named) {
565
+ const absolutePath = path.isAbsolute(named.source_file)
566
+ ? named.source_file
567
+ : path.resolve(this.repoRoot, named.source_file);
568
+ const sourceFile = this.project.getSourceFile(absolutePath);
569
+ if (!sourceFile)
570
+ return undefined;
571
+ const site = declarationSite(sourceFile, named.symbol_name, named.source_file, this.expandOrigin());
572
+ if ('reason' in site)
573
+ return undefined;
574
+ const declaration = site.sourceFile.getInterface(site.name) ??
575
+ site.sourceFile.getTypeAlias(site.name) ??
576
+ site.sourceFile.getClass(site.name) ??
577
+ site.sourceFile.getEnum(site.name);
578
+ return declaration?.getType();
579
+ }
503
580
  /**
504
581
  * carrick#1836: list on `result` the declarations behind the names its text
505
582
  * prints that the request's file cannot resolve (`PrintedTypes.namesIn`).
@@ -915,6 +992,16 @@ export class TypeInferrer {
915
992
  if (provenance && provenance.length > 0) {
916
993
  inferred.any_provenance = provenance;
917
994
  }
995
+ // The relation is asked only of the function the row's own line names.
996
+ // A locator that binds inside a callback of the handler (`user` in
997
+ // `this.users.find((user) => user.id === id)`) reaches this with that
998
+ // callback, whose return is not the route's, and a read of the wrong
999
+ // function must never displace the symbol the model named.
1000
+ if (this.modelSymbols.has(inferred.alias) && this.isTheRowsOwnFunction(request, func)) {
1001
+ this.relateModelSymbol(request, inferred, [
1002
+ unwrapResult.wasUnwrapped ? unwrapResult.payloadType : awaitedType,
1003
+ ]);
1004
+ }
918
1005
  return inferred;
919
1006
  }
920
1007
  /**
@@ -1201,7 +1288,11 @@ export class TypeInferrer {
1201
1288
  typeString = this.expandResolvedTypeStructural(resolved, typeText(resolved, payloadNode), this.wireFormatFor(request));
1202
1289
  }
1203
1290
  const anchor = this.unwrapArrayLevels(this.unwrapPromiseType(payloadType));
1204
- return this.createInferredType(request, typeString, false, this.getNodeLocation(payloadNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth, this.primaryTypeSymbolSource(anchor.element));
1291
+ const inferred = this.createInferredType(request, typeString, false, this.getNodeLocation(payloadNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth, this.primaryTypeSymbolSource(anchor.element));
1292
+ this.relateModelSymbol(request, inferred, [
1293
+ unwrapResult.wasUnwrapped ? unwrapResult.payloadType : this.unwrapPromiseType(payloadType),
1294
+ ]);
1295
+ return inferred;
1205
1296
  }
1206
1297
  /**
1207
1298
  * The handler a module exports on `line`: a function that opens on exactly
@@ -1456,6 +1547,7 @@ export class TypeInferrer {
1456
1547
  const inferred = this.createInferredType(request, recovered.typeString, recovered.isExplicit, this.getNodeLocation(recovered.node), undefined, resolvedSymbol ?? writtenAnchor?.symbol, writtenAnchor ? writtenAnchor.depth : recoveredAnchor?.depth, writtenAnchor ? writtenAnchor.source : resolvedSource);
1457
1548
  if (recovered.responseModes)
1458
1549
  inferred.response_modes = recovered.responseModes;
1550
+ this.relateModelSymbol(request, inferred, recovered.readTypes);
1459
1551
  return inferred;
1460
1552
  }
1461
1553
  /**
@@ -2215,9 +2307,25 @@ export class TypeInferrer {
2215
2307
  if (requestType) {
2216
2308
  return this.declaredRequestInferredType(request, requestType, this.getNodeLocation(registrationHandler));
2217
2309
  }
2218
- // A genuinely payload-less handler (no typed request read): do NOT fall
2219
- // through to read the registration call's return type, which would emit a
2220
- // spurious framework type. Report unresolved, as before this capability.
2310
+ // carrick#827: the handler reads nothing from its parameters, so the
2311
+ // route takes no request body, and that is a decision. Left undecided,
2312
+ // the capture re-reads this same span and publishes the registration
2313
+ // call's own type (`void` wherever the server library resolves) as the
2314
+ // route's request contract.
2315
+ if (this.handlerReadsNothingFromItsParameters(registrationHandler)) {
2316
+ this.log(`Route registration at ${request.file_path}:${request.line_number}: its handler reads ` +
2317
+ 'nothing from its parameters; the route takes no request body');
2318
+ return this.decidedAbstain(request, this.getNodeLocation(registrationHandler), {
2319
+ path: '',
2320
+ kind: 'unknown',
2321
+ reason: 'no_request_body',
2322
+ detail: "the route's handler reads nothing from its parameters (each use of one is a call " +
2323
+ 'whose value it discards or returns), so the route takes no request body',
2324
+ });
2325
+ }
2326
+ // A handler that reads from a parameter and states no type at the read:
2327
+ // do NOT fall through to read the registration call's return type, which
2328
+ // would emit a spurious framework type. Report unresolved, as before.
2221
2329
  this.log(`Route registration at ${request.file_path}:${request.line_number} has no typed request read; leaving unresolved`);
2222
2330
  return null;
2223
2331
  }
@@ -2475,7 +2583,20 @@ export class TypeInferrer {
2475
2583
  if (chainRead) {
2476
2584
  return { terminal: chainRead, projectionOnly: false, projections: [] };
2477
2585
  }
2478
- const returnStmt = callExpr.getFirstAncestorByKind(SyntaxKind.ReturnStatement);
2586
+ // A return statement answers only for a call of the function it returns
2587
+ // from (carrick#2282). The nearest `return` above a call in a callback is
2588
+ // an OUTER function's, and its expression is what that function hands
2589
+ // back with the callback inside it: a command table, a client object, a
2590
+ // UI element. The one callback it does answer for is the executor of a
2591
+ // promise it returns (`return new Promise<T>((resolve) => { request(…,
2592
+ // (res) => resolve(…)) })`): what the caller receives is what that
2593
+ // promise settles with, and the call made in the executor produces it.
2594
+ const nearestReturn = callExpr.getFirstAncestorByKind(SyntaxKind.ReturnStatement);
2595
+ const returnStmt = nearestReturn &&
2596
+ (this.findContainingFunctionForNode(nearestReturn) === func ||
2597
+ this.callIsInReturnedPromiseExecutor(nearestReturn, func))
2598
+ ? nearestReturn
2599
+ : undefined;
2479
2600
  if (returnStmt) {
2480
2601
  const returnExpr = returnStmt.getExpression();
2481
2602
  if (returnExpr) {
@@ -2505,6 +2626,15 @@ export class TypeInferrer {
2505
2626
  const projections = [];
2506
2627
  /** The value itself was read: returned, passed, aliased, or body-read. */
2507
2628
  let sawWholeRead = false;
2629
+ // Where the call's own value states no type, a call that takes it whole
2630
+ // is the first place the source states one, and the walk follows it
2631
+ // (`parse(raw)` after `const raw = await readRaw()`, carrick#1375).
2632
+ const callValue = this.unwrapPromiseType(callExpr.getType());
2633
+ const callValueIsUntyped = callValue.isAny() || callValue.isUnknown();
2634
+ // A transport response handed whole to a helper is a body read through
2635
+ // that helper (`const data: T = await parseJson(response)`), as
2636
+ // `response.json()` is one in place.
2637
+ const callValueIsTransport = this.typeIsOrContainsResponseMachinery(callValue);
2508
2638
  const startPos = callExpr.getStart();
2509
2639
  const candidates = this.collectDefUseNodes(func);
2510
2640
  for (const expr of candidates) {
@@ -2582,25 +2712,113 @@ export class TypeInferrer {
2582
2712
  }
2583
2713
  }
2584
2714
  }
2585
- // The last expression the value flows into is the call's payload — a
2586
- // cast, a parse, an alias. An expression that only reads MEMBERS out
2587
- // of it is a projection and states a part of the payload, never the
2588
- // payload: the hook that derives `query.data?.flags` off a query
2589
- // result published a boolean map as the expected response of a call
2590
- // whose envelope the client declares in full (carrick#1375).
2715
+ // A later expression answers for the call only where it RESTATES the
2716
+ // value: an alias, a cast, a typed binding. An expression that only
2717
+ // reads MEMBERS out of it is a projection and states a part of the
2718
+ // payload, never the payload: the hook that derives
2719
+ // `query.data?.flags` off a query result published a boolean map as
2720
+ // the expected response of a call whose envelope the client declares
2721
+ // in full (carrick#1375). Any other use computes something else from
2722
+ // the value (`describe(page)`, `Math.max(...items.map(…))`,
2723
+ // `rows.map(…)`, `flag && render(row)`): it reads the value whole, and
2724
+ // the answer stays the call's own (carrick#2112). An expression that
2725
+ // takes the body out of a transport response in a form that is not
2726
+ // JSON or text (`new Uint8Array(await res.arrayBuffer())`) holds the
2727
+ // body, and answers as it did.
2591
2728
  if (this.expressionUsesNames(expr, currentNames) &&
2592
2729
  !this.usesNamesOnlyByProjection(expr, currentNames)) {
2593
- lastNode = expr;
2730
+ if (callValueIsUntyped ||
2731
+ this.restatesTrackedValue(expr, currentNames) ||
2732
+ this.holdsBinaryBodyRead(expr, currentNames) ||
2733
+ (callValueIsTransport && this.handsTrackedValueToACall(expr, currentNames))) {
2734
+ lastNode = expr;
2735
+ }
2736
+ else {
2737
+ sawWholeRead = true;
2738
+ }
2594
2739
  }
2595
2740
  }
2596
2741
  return {
2597
- terminal: lastNode,
2742
+ terminal: this.destructuredValueTerminal(lastNode, binding.node, callExpr, callValueIsTransport),
2598
2743
  projectionOnly: projections.length > 0 && !sawWholeRead,
2599
2744
  projections,
2600
2745
  };
2601
2746
  }
2602
2747
  return { terminal: callExpr, projectionOnly: false, projections: [] };
2603
2748
  }
2749
+ /**
2750
+ * Where the walk ends on the call's own destructuring binding (`const {
2751
+ * tasks } = await api.list()`), the declaration has no type of its own: the
2752
+ * checker answers `any` for it. The value it takes apart is the call's, so
2753
+ * the call answers (carrick#2112). Not for a transport value: a helper's
2754
+ * `{ body, response }` read as text would bypass the capture's machinery
2755
+ * guard, so that binding stays unread and the capture answers the row.
2756
+ */
2757
+ destructuredValueTerminal(lastNode, bindingNode, callExpr, callValueIsTransport) {
2758
+ if (lastNode !== bindingNode ||
2759
+ callValueIsTransport ||
2760
+ !Node.isVariableDeclaration(lastNode) ||
2761
+ Node.isIdentifier(lastNode.getNameNode())) {
2762
+ return lastNode;
2763
+ }
2764
+ return callExpr;
2765
+ }
2766
+ /**
2767
+ * `expr` declares a name for what a call returns when it is handed a
2768
+ * tracked name whole (`const data: T = await parseJson(response, 'op')`).
2769
+ */
2770
+ handsTrackedValueToACall(expr, names) {
2771
+ if (!Node.isVariableDeclaration(expr))
2772
+ return false;
2773
+ const init = this.unwrapExpressionNode(expr.getInitializer());
2774
+ return (Node.isCallExpression(init) &&
2775
+ init
2776
+ .getArguments()
2777
+ .some((argument) => this.isIdentifierUsage(this.unwrapExpressionNode(argument), names)));
2778
+ }
2779
+ /** `expr` holds a binary whole-body read taken on a tracked name. */
2780
+ holdsBinaryBodyRead(expr, names) {
2781
+ return expr
2782
+ .getDescendantsOfKind(SyntaxKind.Identifier)
2783
+ .some((id) => this.isIdentifierUsage(id, names) &&
2784
+ this.bodyReadOnReceiver(id, BINARY_BODY_READS) !== undefined);
2785
+ }
2786
+ /**
2787
+ * `func`, the function a call is made in, is the executor of the promise
2788
+ * `returnStmt` returns: `return new Promise<T>((resolve, reject) => { … })`
2789
+ * (carrick#2282). The promise is recognised by the language's await
2790
+ * protocol on the constructed value, never by a name.
2791
+ */
2792
+ callIsInReturnedPromiseExecutor(returnStmt, func) {
2793
+ if (!func || !Node.isReturnStatement(returnStmt))
2794
+ return false;
2795
+ const returned = this.unwrapExpressionNode(returnStmt.getExpression());
2796
+ if (!Node.isNewExpression(returned))
2797
+ return false;
2798
+ const [executor] = returned.getArguments();
2799
+ if (!executor || this.unwrapExpressionNode(executor) !== func)
2800
+ return false;
2801
+ const constructed = returned.getType();
2802
+ return this.unwrapPromiseType(constructed) !== constructed;
2803
+ }
2804
+ /**
2805
+ * `expr` declares a name for the tracked value as it is: its initializer,
2806
+ * with the wrappers that leave a value what it was peeled (parentheses,
2807
+ * `await`, `as`, `satisfies`, `!`), is a tracked name (carrick#2112). The
2808
+ * declaration's type is then the value's, as the source restates it.
2809
+ */
2810
+ restatesTrackedValue(expr, names) {
2811
+ if (!Node.isVariableDeclaration(expr))
2812
+ return false;
2813
+ let value = expr.getInitializer();
2814
+ for (;;) {
2815
+ const peeled = value && this.unwrapExpressionNode(value);
2816
+ value = peeled && Node.isSatisfiesExpression(peeled) ? peeled.getExpression() : peeled;
2817
+ if (value === peeled)
2818
+ break;
2819
+ }
2820
+ return !!value && this.isIdentifierUsage(value, names);
2821
+ }
2604
2822
  /**
2605
2823
  * True when a member read on the call's result resolves to one of that
2606
2824
  * result's own TYPE ARGUMENTS — the source is unwrapping a generic envelope
@@ -2668,6 +2886,10 @@ export class TypeInferrer {
2668
2886
  * as written.
2669
2887
  */
2670
2888
  resultCarrierPayload(type, at, projections, where, callArgs) {
2889
+ const deferred = this.deferredConditionalPayload(type, at, projections, where, callArgs);
2890
+ if (deferred !== undefined) {
2891
+ return deferred;
2892
+ }
2671
2893
  const shape = this.resultCarrierArguments(type, at, callArgs);
2672
2894
  if (!shape) {
2673
2895
  return undefined;
@@ -2736,6 +2958,90 @@ export class TypeInferrer {
2736
2958
  'only non-error one and the source reads none of them out. Not guessing which ' +
2737
2959
  'argument is the payload');
2738
2960
  }
2961
+ /**
2962
+ * carrick#1841: the branches of a conditional type the compiler left
2963
+ * deferred on a type parameter, each with its promise-like peeled, or
2964
+ * `undefined` for any other type.
2965
+ *
2966
+ * Inside a generic function whose result depends on a type parameter it has
2967
+ * not fixed (`<Throw extends boolean>(o) => client.get<Table, Errors,
2968
+ * Throw>(…)`), the call's result is such a conditional. The compiler states
2969
+ * what its branches allow itself: the constraint of the conditional, which
2970
+ * is its apparent type. No branch is read off the alias's declaration.
2971
+ */
2972
+ deferredConditionalBranches(type) {
2973
+ const compilerType = type.compilerType;
2974
+ if (!(compilerType.flags & ts.TypeFlags.Conditional))
2975
+ return undefined;
2976
+ const checked = compilerType.checkType;
2977
+ if (!(checked.flags & ts.TypeFlags.TypeParameter))
2978
+ return undefined;
2979
+ let apparent;
2980
+ try {
2981
+ apparent = type.getApparentType();
2982
+ }
2983
+ catch {
2984
+ return undefined;
2985
+ }
2986
+ if (apparent === type || apparent.isUnknown() || apparent.isAny())
2987
+ return undefined;
2988
+ const members = apparent.isUnion() ? apparent.getUnionTypes() : [apparent];
2989
+ return members.map((member) => this.unwrapThenableType(this.unwrapPromiseType(member)));
2990
+ }
2991
+ /**
2992
+ * carrick#1841: what a call result deferred on a type parameter carries,
2993
+ * read through its branches. `undefined` where `type` is no such
2994
+ * conditional, or none of its branches is a result carrier, and the type
2995
+ * keeps the answer it had.
2996
+ *
2997
+ * Each branch that is a carrier is read by the carrier read, with the
2998
+ * call's own type arguments. Every one of them must name the same payload,
2999
+ * by the compiler's type identity, and every other branch must hold that
3000
+ * payload as the type of one of its members (the branch a client answers
3001
+ * with when it throws on failure holds the body and no error side). Then
3002
+ * the payload is the body whichever branch the caller fixes. A carrier
3003
+ * branch that states no contract decides for the whole. Branches that
3004
+ * disagree state none: the conditional is the client's bookkeeping and is
3005
+ * never published as written.
3006
+ */
3007
+ deferredConditionalPayload(type, at, projections, where, callArgs) {
3008
+ const branches = this.deferredConditionalBranches(type);
3009
+ if (!branches)
3010
+ return undefined;
3011
+ const reads = branches.map((branch) => ({
3012
+ branch,
3013
+ read: this.resultCarrierPayload(branch, at, projections, where, callArgs),
3014
+ }));
3015
+ const carriers = reads.flatMap((entry) => (entry.read ? [entry.read] : []));
3016
+ if (carriers.length === 0)
3017
+ return undefined;
3018
+ const abstained = carriers.find((read) => 'abstain' in read);
3019
+ if (abstained)
3020
+ return abstained;
3021
+ const [{ payload }] = carriers;
3022
+ const isPayload = (candidate) => candidate.compilerType === payload.compilerType;
3023
+ if (!carriers.every((read) => isPayload(read.payload))) {
3024
+ return {
3025
+ abstain: "the call's result depends on a type parameter and its branches carry different " +
3026
+ 'bodies, so this site states no response contract',
3027
+ };
3028
+ }
3029
+ const holdsPayload = (branch) => branch.getProperties().some((property) => {
3030
+ try {
3031
+ return isPayload(property.getTypeAtLocation(at));
3032
+ }
3033
+ catch {
3034
+ return false;
3035
+ }
3036
+ });
3037
+ if (!reads.every((entry) => entry.read !== undefined || holdsPayload(entry.branch))) {
3038
+ return {
3039
+ abstain: "the call's result depends on a type parameter and one of its branches does not " +
3040
+ 'hold the body the others carry, so this site states no response contract',
3041
+ };
3042
+ }
3043
+ return { payload };
3044
+ }
2739
3045
  /**
2740
3046
  * The value of a response table's one success row, for a table a carrier
2741
3047
  * holds as itself rather than through its body. `undefined` where the table
@@ -2788,6 +3094,17 @@ export class TypeInferrer {
2788
3094
  * holds as a member. `undefined` when `type` is not a carrier.
2789
3095
  */
2790
3096
  resultCarrierArguments(type, at, callArgs = []) {
3097
+ // carrick#1841: a conditional deferred on a type parameter is a carrier
3098
+ // where one of its branches is.
3099
+ const deferredBranches = this.deferredConditionalBranches(type);
3100
+ if (deferredBranches) {
3101
+ for (const branch of deferredBranches) {
3102
+ const shape = this.resultCarrierArguments(branch, at, callArgs);
3103
+ if (shape)
3104
+ return shape;
3105
+ }
3106
+ return undefined;
3107
+ }
2791
3108
  const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
2792
3109
  if (!carrier.isUnion()) {
2793
3110
  return undefined;
@@ -3278,14 +3595,15 @@ export class TypeInferrer {
3278
3595
  * The zero-argument whole-body read that takes `receiver` as its receiver,
3279
3596
  * `res.json()` or `res.text()`, or `undefined`. A text read is a body read
3280
3597
  * like a json one (carrick#1842): without it, `return res.text()` left the
3281
- * walk on the response binding and published the transport object.
3598
+ * walk on the response binding and published the transport object. `reads`
3599
+ * names the reads asked for.
3282
3600
  */
3283
- bodyReadOnReceiver(receiver) {
3601
+ bodyReadOnReceiver(receiver, reads = WHOLE_BODY_READS) {
3284
3602
  const access = receiver.getParent();
3285
3603
  if (!access ||
3286
3604
  !Node.isPropertyAccessExpression(access) ||
3287
3605
  access.getExpression() !== receiver ||
3288
- !WHOLE_BODY_READS.has(access.getName())) {
3606
+ !reads.has(access.getName())) {
3289
3607
  return undefined;
3290
3608
  }
3291
3609
  const call = access.getParent();
@@ -4126,6 +4444,12 @@ export class TypeInferrer {
4126
4444
  */
4127
4445
  expandResolvedTypeStructural(type, fallback, wire = 'declared') {
4128
4446
  try {
4447
+ // carrick#2116: a narrowed `unknown` or `object`, at the root or as a
4448
+ // member of the root union, states no shape. It would print `{}` or
4449
+ // `object` and be judged as a body that lacks every field.
4450
+ if (statesNoShape(this.project.getTypeChecker().compilerObject, type.compilerType)) {
4451
+ return 'unknown';
4452
+ }
4129
4453
  const expanded = expandTypeStructural(type, this.expandOrigin(), { wire });
4130
4454
  // A wire print that differs from the declared one is the answer even
4131
4455
  // without an inlined object: a bare `Date` payload sends a string.
@@ -4276,6 +4600,7 @@ export class TypeInferrer {
4276
4600
  // A union joined several payloads, so there is no single anchor — the
4277
4601
  // same rule the wrapper-unwrap path applies.
4278
4602
  anchorType: distinct.length === 1 ? distinct[0].anchorType : undefined,
4603
+ readTypes: distinct.map((c) => c.anchorType),
4279
4604
  statedTypeNode: distinct.length === 1 ? distinct[0].statedTypeNode : undefined,
4280
4605
  };
4281
4606
  }
@@ -5914,6 +6239,130 @@ export class TypeInferrer {
5914
6239
  }
5915
6240
  return undefined;
5916
6241
  }
6242
+ /**
6243
+ * True when `handler` reads nothing from the incoming message (carrick#827):
6244
+ * every use of one of its parameters, named or destructured, is the receiver
6245
+ * of a call chain whose value the handler discards or returns
6246
+ * (`reply.json(result)`, `return c.status(201).json(result)`). That is a
6247
+ * send. A returned chain must end in a call that takes an argument, so a
6248
+ * read handed straight back (`return request.json()`) is not one.
6249
+ *
6250
+ * Anything else may be a read of the body and makes this false: a property
6251
+ * read, a call whose value is used, a parameter handed whole to another
6252
+ * function, or a call in the chain handed an argument that can receive what
6253
+ * the parameter gives (`argumentCanReceive`: a callback, an object with a
6254
+ * method), as in `request.pipe(dest)`, `request.on('data', cb)` and
6255
+ * `request.json().then(cb)`. No method or property name is consulted. A
6256
+ * handler with no parameter, or one that uses `this`, proves nothing: it may
6257
+ * read the message from `this` or a closure.
6258
+ */
6259
+ handlerReadsNothingFromItsParameters(handler) {
6260
+ // A declared `this` parameter, or any `this` in the body, is a way in the
6261
+ // walk below does not follow.
6262
+ const parameters = new Set(handler.getParameters().filter((parameter) => parameter.getName() !== 'this'));
6263
+ if (parameters.size === 0)
6264
+ return false;
6265
+ const body = handler.getBody();
6266
+ if (!body)
6267
+ return false;
6268
+ if (body.getFirstDescendantByKind(SyntaxKind.ThisKeyword))
6269
+ return false;
6270
+ const namesAParameter = (identifier) => (identifier.getSymbol()?.getDeclarations() ?? []).some((declaration) => {
6271
+ const parameter = Node.isParameterDeclaration(declaration)
6272
+ ? declaration
6273
+ : Node.isBindingElement(declaration)
6274
+ ? declaration.getFirstAncestorByKind(SyntaxKind.Parameter)
6275
+ : undefined;
6276
+ return parameter !== undefined && parameters.has(parameter);
6277
+ });
6278
+ for (const identifier of body.getDescendantsOfKind(SyntaxKind.Identifier)) {
6279
+ if (!namesAParameter(identifier))
6280
+ continue;
6281
+ // Climb the call chain the parameter is the root receiver of.
6282
+ let top = identifier;
6283
+ let lastCall;
6284
+ for (;;) {
6285
+ const parent = top.getParent();
6286
+ if (!parent)
6287
+ break;
6288
+ if (Node.isPropertyAccessExpression(parent) && parent.getExpression() === top) {
6289
+ top = parent;
6290
+ }
6291
+ else if (Node.isCallExpression(parent) && parent.getExpression() === top) {
6292
+ // A send hands the parameter data; an argument able to receive may
6293
+ // be where the body goes.
6294
+ if (parent.getArguments().some((argument) => this.argumentCanReceive(argument))) {
6295
+ return false;
6296
+ }
6297
+ lastCall = parent;
6298
+ top = parent;
6299
+ }
6300
+ else if ((Node.isAwaitExpression(parent) ||
6301
+ Node.isParenthesizedExpression(parent) ||
6302
+ Node.isNonNullExpression(parent)) &&
6303
+ parent.getExpression() === top) {
6304
+ top = parent;
6305
+ }
6306
+ else {
6307
+ break;
6308
+ }
6309
+ }
6310
+ // The chain has to end in a call: a property read yields a value.
6311
+ if (!lastCall || this.unwrapExpressionNode(top) !== lastCall)
6312
+ return false;
6313
+ const holder = top.getParent();
6314
+ const discarded = !!holder && Node.isExpressionStatement(holder);
6315
+ const returned = !!holder &&
6316
+ (Node.isReturnStatement(holder) || (holder === handler && handler.getBody() === top)) &&
6317
+ lastCall.getArguments().length > 0;
6318
+ if (!discarded && !returned)
6319
+ return false;
6320
+ }
6321
+ return true;
6322
+ }
6323
+ /**
6324
+ * True unless `argument` is proven to be data that cannot receive anything
6325
+ * from the call it is handed to (carrick#827). Data is a primitive or literal,
6326
+ * `null`/`undefined`, or an object with no call or construct signature and
6327
+ * no callable member; an array of such, or a union all of whose members are.
6328
+ * A callback, an object with a method (a stream, an emitter), or a type the
6329
+ * compiler cannot state (`any`, `unknown`, a type parameter) can receive.
6330
+ * Sibling of `callResultIsPlainData`, which reads a call's result and so
6331
+ * rejects primitives and empty objects; an argument of either is data.
6332
+ */
6333
+ argumentCanReceive(argument) {
6334
+ const canReceive = (type, depth) => {
6335
+ if (depth > 3)
6336
+ return true;
6337
+ if (type.isAny() || type.isUnknown() || type.isTypeParameter())
6338
+ return true;
6339
+ if (type.isUnion()) {
6340
+ return type.getUnionTypes().some((member) => canReceive(member, depth + 1));
6341
+ }
6342
+ if (type.isIntersection()) {
6343
+ return type.getIntersectionTypes().some((member) => canReceive(member, depth + 1));
6344
+ }
6345
+ const element = type.getArrayElementType();
6346
+ if (element)
6347
+ return canReceive(element, depth + 1);
6348
+ if (type.isTuple()) {
6349
+ return type.getTupleElements().some((member) => canReceive(member, depth + 1));
6350
+ }
6351
+ if (!type.isObject())
6352
+ return false;
6353
+ if (this.isCallableType(type))
6354
+ return true;
6355
+ const indexed = type.getStringIndexType() ?? type.getNumberIndexType();
6356
+ const callable = (held) => held.isAny() ||
6357
+ (held.isUnion()
6358
+ ? held.getUnionTypes().some((member) => this.isCallableType(member))
6359
+ : this.isCallableType(held));
6360
+ if (indexed && callable(indexed))
6361
+ return true;
6362
+ return type.getProperties().some((member) => callable(member.getTypeAtLocation(argument)));
6363
+ };
6364
+ return canReceive(argument.getType(), 0);
6365
+ }
5917
6366
  /**
5918
6367
  * Scan a handler function body for the FIRST request-body read that carries a
5919
6368
  * type, and return that type's structural text — or null when the body holds
@@ -7489,6 +7938,20 @@ export class TypeInferrer {
7489
7938
  const MIN_REVERSE_MATCH_COVERAGE = 0.5;
7490
7939
  const containingMatches = candidates.filter((c) => c.text.includes(normalizedTarget));
7491
7940
  if (containingMatches.length > 0) {
7941
+ // A text that is only a wrapped call's first line (`load({`) is held by
7942
+ // every sibling that opens alike, and the smallest of them is not the
7943
+ // one the locator names (carrick#2114). The nodes whose own text OPENS
7944
+ // with the text are the ones a first line names: among those the stated
7945
+ // line decides, then size. A fragment from the middle of a node opens
7946
+ // nothing, and stays on the smallest node holding it.
7947
+ const opening = lineNumber === undefined
7948
+ ? []
7949
+ : containingMatches.filter((c) => c.text.startsWith(normalizedTarget));
7950
+ if (lineNumber !== undefined && opening.length > 0) {
7951
+ const distance = (c) => Math.abs(startLineOf(c.node) - lineNumber);
7952
+ const nearest = Math.min(...opening.map(distance));
7953
+ return this.pickBestMatch(opening.filter((c) => distance(c) === nearest).map((c) => c.node), lineNumber);
7954
+ }
7492
7955
  return this.pickBestMatch(containingMatches.map((c) => c.node), lineNumber);
7493
7956
  }
7494
7957
  const reverseMatches = candidates.filter((c) => c.text.length >= MIN_REVERSE_MATCH_LEN &&