carrick 0.3.124 → 0.3.126

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.
Files changed (29) hide show
  1. package/package.json +6 -6
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/sidecar/dist/src/capture/api.d.ts +5 -1
  4. package/sidecar/dist/src/capture/check-classify.js +11 -5
  5. package/sidecar/dist/src/capture/check.js +8 -1
  6. package/sidecar/dist/src/capture/deep-walk.d.ts +31 -0
  7. package/sidecar/dist/src/capture/deep-walk.js +43 -1
  8. package/sidecar/dist/src/capture/deno-project.d.ts +7 -0
  9. package/sidecar/dist/src/capture/deno-project.js +7 -0
  10. package/sidecar/dist/src/capture/index.d.ts +1 -1
  11. package/sidecar/dist/src/capture/index.js +1 -1
  12. package/sidecar/dist/src/capture/service-config.d.ts +11 -0
  13. package/sidecar/dist/src/capture/service-config.js +40 -0
  14. package/sidecar/dist/src/declaration-attribution.d.ts +11 -0
  15. package/sidecar/dist/src/declaration-attribution.js +16 -0
  16. package/sidecar/dist/src/implementations.d.ts +43 -0
  17. package/sidecar/dist/src/implementations.js +224 -0
  18. package/sidecar/dist/src/index.js +35 -1
  19. package/sidecar/dist/src/library-claims.d.ts +17 -2
  20. package/sidecar/dist/src/library-claims.js +242 -47
  21. package/sidecar/dist/src/position-rows.d.ts +119 -1
  22. package/sidecar/dist/src/position-rows.js +163 -9
  23. package/sidecar/dist/src/project-loader.d.ts +31 -0
  24. package/sidecar/dist/src/project-loader.js +67 -4
  25. package/sidecar/dist/src/type-inferrer.d.ts +150 -16
  26. package/sidecar/dist/src/type-inferrer.js +651 -94
  27. package/sidecar/dist/src/types.d.ts +46 -2
  28. package/sidecar/dist/src/validators.d.ts +73 -0
  29. package/sidecar/dist/src/validators.js +10 -0
@@ -190,6 +190,18 @@ const SERIALISED_BODY_UNREAD_DETAIL = {
190
190
  };
191
191
  /** `any` or `unknown` as a type in printed type text, not as a member name or a string. */
192
192
  const TOP_TYPE_TOKEN = /(?<![\w$'"`])(?:any|unknown)(?![\w$'"`?:])/;
193
+ /**
194
+ * The type an assertion states, or undefined when it states none. A const
195
+ * assertion (`x as const`, `<const>x`) narrows its operand's own type and
196
+ * names no type: its type node prints as `const`, a name nothing declares
197
+ * (carrick#2590). The walk stops there rather than reaching for an assertion
198
+ * further out, which describes a larger value than the read.
199
+ */
200
+ function statedAssertionType(typeNode) {
201
+ if (!typeNode || ts.isConstTypeReference(typeNode.compilerNode))
202
+ return undefined;
203
+ return typeNode;
204
+ }
193
205
  /**
194
206
  * The items whose whitespace-collapsed texts differ, first of each kept, in
195
207
  * order: how a joined union drops a member it already has.
@@ -1996,6 +2008,27 @@ export class TypeInferrer {
1996
2008
  });
1997
2009
  }
1998
2010
  typeString = this.unwrapPromise(typeString, returnType);
2011
+ // carrick#2594: the call's value is untyped (`Promise<any>`,
2012
+ // `Promise<unknown>`), and the function it is made in states its type:
2013
+ // it returns the value from a function with a declared return type, holds
2014
+ // it in an annotated name, validates it with a schema, narrows it with a
2015
+ // function that returns it unchanged, casts it whole, or hands it to a
2016
+ // typed parameter. A function that converts the value into something new
2017
+ // states nothing about it, and neither does a use on a failure path.
2018
+ const untypedAnswer = typeString.trim() === 'any' || typeString.trim() === 'unknown';
2019
+ if (untypedAnswer && func && !explicitType && !unwrapResult.wasUnwrapped && !carrierPayload) {
2020
+ const stated = this.statedInFunction(callExpr, func, {
2021
+ returned: true,
2022
+ parameters: true,
2023
+ skipFailurePaths: true,
2024
+ });
2025
+ if (stated) {
2026
+ this.log(`Call result at ${where} is untyped, and its function states its type ` +
2027
+ `(${stated.how}); publishing that type`);
2028
+ typeString = stated.text;
2029
+ isExplicit = true;
2030
+ }
2031
+ }
1999
2032
  // #336: consumer analogue of the #306 producer anchor. Anchor on the
2000
2033
  // CALL's own payload (the rule-extracted Type, else the awaited call
2001
2034
  // type), peeling array levels to the element symbol + depth. Without the
@@ -2532,7 +2565,24 @@ export class TypeInferrer {
2532
2565
  // is its response, and a config's type is the config, so either one read
2533
2566
  // as the body publishes the wrong contract. Follow the call's signature to
2534
2567
  // the argument it sends as the body.
2535
- const inCall = this.requestBodyInCall(unwrapped);
2568
+ let inCall = this.requestBodyInCall(unwrapped);
2569
+ if (inCall.kind === 'none') {
2570
+ // carrick#2628: the argument goes into a function that builds the body
2571
+ // itself. An abstain here is a decision: capture's own re-read of the
2572
+ // locator would publish the argument as the body.
2573
+ const built = this.bodyBuiltByCallee(unwrapped);
2574
+ if (built.kind === 'abstain') {
2575
+ this.log(`Request locator at ${request.file_path}:${request.line_number}: ${built.why}; ` +
2576
+ 'the argument is not the body');
2577
+ return this.decidedAbstain(request, this.getNodeLocation(unwrapped), {
2578
+ path: '',
2579
+ kind: 'unknown',
2580
+ reason: 'body_built_by_callee',
2581
+ detail: `the located argument is not the request body: ${built.why}`,
2582
+ });
2583
+ }
2584
+ inCall = built;
2585
+ }
2536
2586
  if (inCall.kind === 'body') {
2537
2587
  this.log(`Request locator at ${request.file_path}:${request.line_number} names ${inCall.from}; ` +
2538
2588
  'reading the body argument its signature declares');
@@ -2659,18 +2709,22 @@ export class TypeInferrer {
2659
2709
  isExplicit = true;
2660
2710
  }
2661
2711
  // carrick#1166: the read itself is untyped (`await c.req.json()` is `any`
2662
- // or `unknown`), and the handler validates it by handing it to a schema:
2663
- // `Schema.safeParse(body)`, `Schema.parse(await c.req.json())`. The schema
2664
- // declares what a caller may send, so its INPUT is the request contract,
2665
- // read the way every other request anchor reads a schema (carrick#1101).
2712
+ // or `unknown`), and the handler states its type further on: it validates
2713
+ // it with a schema (`Schema.safeParse(body ?? {})`, whose INPUT is the
2714
+ // request contract, carrick#1101), assigns it to an annotated name
2715
+ // (`let body: T; body = await c.req.json()`), narrows it with a function
2716
+ // that returns it unchanged, or casts the whole value (carrick#2594).
2666
2717
  const readType = this.unwrapPromiseType(payloadType);
2667
2718
  if (!explicitType &&
2668
2719
  !unwrapResult.wasUnwrapped &&
2669
2720
  (readType.isAny() || readType.isUnknown())) {
2670
- const parsedWith = this.schemaConsumingRead(node);
2721
+ const func = this.findContainingFunctionForNode(node);
2722
+ const parsedWith = func
2723
+ ? this.statedInFunction(node, func, { returned: false, parameters: false, skipFailurePaths: false })
2724
+ : null;
2671
2725
  if (parsedWith) {
2672
2726
  this.log(`Request locator at ${request.file_path}:${request.line_number} is an untyped read ` +
2673
- "the handler validates with a schema; publishing the schema's input");
2727
+ `its handler states a type for (${parsedWith.how}); publishing that type`);
2674
2728
  return this.declaredRequestInferredType(request, parsedWith, this.getNodeLocation(node));
2675
2729
  }
2676
2730
  }
@@ -2711,9 +2765,14 @@ export class TypeInferrer {
2711
2765
  // (res) => resolve(…)) })`): what the caller receives is what that
2712
2766
  // promise settles with, and the call made in the executor produces it.
2713
2767
  const nearestReturn = callExpr.getFirstAncestorByKind(SyntaxKind.ReturnStatement);
2768
+ // A return whose value is an object or array literal built around the
2769
+ // call (`return { ok: true, count: countOf(await client.add(…)) }`) hands
2770
+ // back the caller's own value, which holds the result as one part: it
2771
+ // answers for the call no more than a command table does (carrick#2628).
2714
2772
  const returnStmt = nearestReturn &&
2715
2773
  (this.findContainingFunctionForNode(nearestReturn) === func ||
2716
- this.callIsInReturnedPromiseExecutor(nearestReturn, func))
2774
+ this.callIsInReturnedPromiseExecutor(nearestReturn, func)) &&
2775
+ !this.callIsPartOfReturnedLiteral(callExpr, nearestReturn)
2717
2776
  ? nearestReturn
2718
2777
  : undefined;
2719
2778
  if (returnStmt) {
@@ -2727,8 +2786,9 @@ export class TypeInferrer {
2727
2786
  // the response, so the walk below has nothing to follow, and the call
2728
2787
  // itself would stand as the terminal and publish the transport object.
2729
2788
  // The read is the terminal, as it is when the walk finds it on a binding,
2730
- // and a call that takes the read and states what it returns says more
2731
- // than the read does (carrick#1382).
2789
+ // and a call that takes the read and states it (a schema, or a function
2790
+ // that returns it unchanged) says more than the read does (carrick#1382,
2791
+ // carrick#2595).
2732
2792
  const inPlaceRead = this.bodyReadOnCallValue(callExpr);
2733
2793
  if (inPlaceRead) {
2734
2794
  return {
@@ -2746,8 +2806,11 @@ export class TypeInferrer {
2746
2806
  /** The value itself was read: returned, passed, aliased, or body-read. */
2747
2807
  let sawWholeRead = false;
2748
2808
  // Where the call's own value states no type, a call that takes it whole
2749
- // is the first place the source states one, and the walk follows it
2750
- // (`parse(raw)` after `const raw = await readRaw()`, carrick#1375).
2809
+ // and states it (a schema, or a function that returns it unchanged) is
2810
+ // the first place the source states one, and the walk follows it
2811
+ // (`parse(raw)` after `const raw = await readRaw()`, carrick#1375). A
2812
+ // converter, or a declaration whose body cannot be read, states nothing
2813
+ // about the value (carrick#2595).
2751
2814
  const callValue = this.unwrapPromiseType(callExpr.getType());
2752
2815
  const callValueIsUntyped = callValue.isAny() || callValue.isUnknown();
2753
2816
  // A transport response handed whole to a helper is a body read through
@@ -2846,7 +2909,7 @@ export class TypeInferrer {
2846
2909
  // body, and answers as it did.
2847
2910
  if (this.expressionUsesNames(expr, currentNames) &&
2848
2911
  !this.usesNamesOnlyByProjection(expr, currentNames)) {
2849
- if (callValueIsUntyped ||
2912
+ if ((callValueIsUntyped && this.statesTrackedValue(expr, currentNames)) ||
2850
2913
  this.restatesTrackedValue(expr, currentNames) ||
2851
2914
  this.holdsBinaryBodyRead(expr, currentNames) ||
2852
2915
  (callValueIsTransport && this.handsTrackedValueToACall(expr, currentNames))) {
@@ -2882,18 +2945,51 @@ export class TypeInferrer {
2882
2945
  }
2883
2946
  return callExpr;
2884
2947
  }
2948
+ /**
2949
+ * Between `callExpr` and the return statement around it there is an object
2950
+ * or array literal: the call's result is one part of a value the function
2951
+ * builds and returns.
2952
+ */
2953
+ callIsPartOfReturnedLiteral(callExpr, returnStmt) {
2954
+ const ancestors = callExpr.getAncestors();
2955
+ return ancestors
2956
+ .slice(0, ancestors.indexOf(returnStmt))
2957
+ .some((a) => Node.isObjectLiteralExpression(a) || Node.isArrayLiteralExpression(a));
2958
+ }
2885
2959
  /**
2886
2960
  * `expr` declares a name for what a call returns when it is handed a
2887
2961
  * tracked name whole (`const data: T = await parseJson(response, 'op')`).
2888
2962
  */
2889
2963
  handsTrackedValueToACall(expr, names) {
2890
- if (!Node.isVariableDeclaration(expr))
2891
- return false;
2892
- const init = this.unwrapExpressionNode(expr.getInitializer());
2893
- return (Node.isCallExpression(init) &&
2894
- init
2895
- .getArguments()
2896
- .some((argument) => this.isIdentifierUsage(this.unwrapExpressionNode(argument), names)));
2964
+ return Node.isVariableDeclaration(expr) && this.callTakingTrackedValue(expr, names) !== undefined;
2965
+ }
2966
+ /**
2967
+ * The call whose result `expr` declares or assigns a name for, and the
2968
+ * index of its argument that is a tracked name whole, or undefined.
2969
+ */
2970
+ callTakingTrackedValue(expr, names) {
2971
+ const value = Node.isVariableDeclaration(expr)
2972
+ ? expr.getInitializer()
2973
+ : Node.isBinaryExpression(expr) && expr.getOperatorToken().getKind() === SyntaxKind.EqualsToken
2974
+ ? expr.getRight()
2975
+ : undefined;
2976
+ const init = this.unwrapExpressionNode(value);
2977
+ if (!Node.isCallExpression(init))
2978
+ return undefined;
2979
+ const index = init
2980
+ .getArguments()
2981
+ .findIndex((argument) => this.isIdentifierUsage(this.unwrapExpressionNode(argument), names));
2982
+ return index >= 0 ? { call: init, index } : undefined;
2983
+ }
2984
+ /**
2985
+ * `expr` names the result of a call that states the tracked value it takes
2986
+ * (`callStatesItsArgument`): a schema validates it, or the callee returns it
2987
+ * unchanged. A converter's result describes what it built from the value,
2988
+ * not the value (carrick#2595).
2989
+ */
2990
+ statesTrackedValue(expr, names) {
2991
+ const taking = this.callTakingTrackedValue(expr, names);
2992
+ return !!taking && this.callStatesItsArgument(taking.call, taking.index);
2897
2993
  }
2898
2994
  /** `expr` holds a binary whole-body read taken on a tracked name. */
2899
2995
  holdsBinaryBodyRead(expr, names) {
@@ -3480,12 +3576,18 @@ export class TypeInferrer {
3480
3576
  * serialiser answering `string` describes what the caller did with the
3481
3577
  * body, not what the body is, and publishing it would be a
3482
3578
  * concrete-but-wrong contract where the honest `any` of the read is
3483
- * merely unresolved.
3579
+ * merely unresolved;
3580
+ * - the call states its argument (`callStatesItsArgument`): a schema
3581
+ * validates it, or the callee returns it unchanged. A converter's
3582
+ * object result describes what it built, not the body (carrick#2595).
3484
3583
  */
3485
3584
  statedPayloadAroundBodyRead(bodyRead) {
3486
3585
  return this.receivingCallOf(bodyRead, (candidate) => Node.isCallExpression(candidate) &&
3487
3586
  this.callResultIsBound(candidate) &&
3488
- this.isObjectShape(this.unwrapPromiseType(candidate.getType())));
3587
+ this.isObjectShape(this.unwrapPromiseType(candidate.getType())) &&
3588
+ this.callStatesItsArgument(candidate, candidate
3589
+ .getArguments()
3590
+ .findIndex((argument) => argument.getStart() <= bodyRead.getStart() && bodyRead.getEnd() <= argument.getEnd())));
3489
3591
  }
3490
3592
  /**
3491
3593
  * The source keeps this call's result: it initializes a declaration, is
@@ -4490,22 +4592,14 @@ export class TypeInferrer {
4490
4592
  const asExpr = Node.isAsExpression(node)
4491
4593
  ? node
4492
4594
  : node.getFirstAncestorByKind(SyntaxKind.AsExpression);
4493
- if (asExpr) {
4494
- const typeNode = asExpr.getTypeNode();
4495
- if (typeNode) {
4496
- return typeNode;
4497
- }
4595
+ const asType = statedAssertionType(asExpr?.getTypeNode());
4596
+ if (asType) {
4597
+ return asType;
4498
4598
  }
4499
4599
  const typeAssertion = Node.isTypeAssertion(node)
4500
4600
  ? node
4501
4601
  : node.getFirstAncestorByKind(SyntaxKind.TypeAssertionExpression);
4502
- if (typeAssertion) {
4503
- const typeNode = typeAssertion.getTypeNode();
4504
- if (typeNode) {
4505
- return typeNode;
4506
- }
4507
- }
4508
- return undefined;
4602
+ return statedAssertionType(typeAssertion?.getTypeNode());
4509
4603
  }
4510
4604
  /**
4511
4605
  * Render an explicit annotation (`as T`, `<T>`, or a typed binding) as
@@ -6606,8 +6700,10 @@ export class TypeInferrer {
6606
6700
  * - a cast on it: `(await request.json()) as NewWidget`;
6607
6701
  * - the annotation of the binding it initialises:
6608
6702
  * `const input: NewWidget = await request.json()`;
6609
- * - a schema the read, or the binding that holds it, is handed to
6610
- * (`schemaConsumingRead`): the schema's input is what a caller may send.
6703
+ * - a type the handler states for the read further on
6704
+ * (`statedInFunction`): a schema it is handed to, the annotation of a
6705
+ * name it is assigned to, a pass-through function, a cast of the whole
6706
+ * value.
6611
6707
  *
6612
6708
  * The first two are `typeNodeStatedAtRead`, the reading a consumer's
6613
6709
  * response read already gets. An annotation further out is not one of them:
@@ -6623,10 +6719,13 @@ export class TypeInferrer {
6623
6719
  'reads off the platform request, as the source types that read');
6624
6720
  return this.createInferredType(request, statedText, true, this.getNodeLocation(read));
6625
6721
  }
6626
- const validated = this.schemaConsumingRead(read);
6722
+ const func = this.findContainingFunctionForNode(read);
6723
+ const validated = func
6724
+ ? this.statedInFunction(read, func, { returned: false, parameters: false, skipFailurePaths: false })
6725
+ : null;
6627
6726
  if (validated) {
6628
6727
  this.log(`Request at ${request.file_path}:${request.line_number} is the body its handler ` +
6629
- "reads off the platform request and validates with a schema; publishing the schema's input");
6728
+ `reads off the platform request and states a type for (${validated.how}); publishing that type`);
6630
6729
  return this.declaredRequestInferredType(request, validated, this.getNodeLocation(read));
6631
6730
  }
6632
6731
  return null;
@@ -6921,6 +7020,231 @@ export class TypeInferrer {
6921
7020
  }
6922
7021
  return { kind: 'none' };
6923
7022
  }
7023
+ /**
7024
+ * carrick#2628: the located node is an argument of a call into a function
7025
+ * whose body the program holds (an in-repo client method,
7026
+ * `client.addTags(project, unique(tags))`), at a parameter that is not a
7027
+ * body slot. What the call sends is decided inside that function, so the
7028
+ * argument is the body only when the function sends it unchanged. One
7029
+ * level, read off each use of the parameter in the function's body:
7030
+ *
7031
+ * - the use IS the value of a body-named member (`body: tags`,
7032
+ * `body: JSON.stringify(tags)`) or of a body slot of a call: the
7033
+ * argument is the body, and its own type stands (`none`);
7034
+ * - the use sits inside such a value (`body: { tags }`,
7035
+ * `body: (event ?? {}) as never`): the body is that value, read at the
7036
+ * function without the casts around it;
7037
+ * - the use sits inside an object or array literal that is no body value
7038
+ * (`send(project, [labels])`): the body cannot be told, abstain.
7039
+ *
7040
+ * Uses that disagree abstain. A generic function's body may be typed by
7041
+ * its type parameters, so it abstains too. A library's declaration
7042
+ * (`fetch`, an installed client) has no body to read and is left alone,
7043
+ * and so is an untyped argument (a handler's `request.body` handed to a
7044
+ * helper): it states no contract to mistake, and the untyped-read path
7045
+ * below reads it.
7046
+ */
7047
+ bodyBuiltByCallee(node) {
7048
+ const own = node.getType();
7049
+ if (own.isAny() || own.isUnknown())
7050
+ return { kind: 'none' };
7051
+ const call = this.receivingCallOf(node, (candidate) => Node.isCallExpression(candidate));
7052
+ if (!call || !Node.isCallExpression(call))
7053
+ return { kind: 'none' };
7054
+ const index = call
7055
+ .getArguments()
7056
+ .findIndex((a) => a.getPos() <= node.getPos() && node.getEnd() <= a.getEnd());
7057
+ if (index < 0)
7058
+ return { kind: 'none' };
7059
+ if (this.requestBodySlots(call)[index])
7060
+ return { kind: 'none' };
7061
+ const callee = this.project.getTypeChecker().getResolvedSignature(call)?.getDeclaration();
7062
+ if (!callee ||
7063
+ !(Node.isFunctionDeclaration(callee) ||
7064
+ Node.isMethodDeclaration(callee) ||
7065
+ Node.isArrowFunction(callee) ||
7066
+ Node.isFunctionExpression(callee))) {
7067
+ return { kind: 'none' };
7068
+ }
7069
+ const body = callee.getBody();
7070
+ if (!body)
7071
+ return { kind: 'none' };
7072
+ const program = this.project.getProgram().compilerObject;
7073
+ if (isExternalOrigin(program, callee.getSourceFile().compilerNode, this.repoRoot, externalImportsOf(this.project))) {
7074
+ return { kind: 'none' };
7075
+ }
7076
+ const param = callee.getParameters()[index];
7077
+ if (!param || param.isRestParameter())
7078
+ return { kind: 'none' };
7079
+ const paramName = param.getNameNode();
7080
+ if (!Node.isIdentifier(paramName))
7081
+ return { kind: 'none' };
7082
+ const paramSymbol = paramName.getSymbol()?.compilerSymbol;
7083
+ if (!paramSymbol)
7084
+ return { kind: 'none' };
7085
+ const checker = this.project.getTypeChecker().compilerObject;
7086
+ const uses = body.getDescendantsOfKind(SyntaxKind.Identifier).filter((id) => {
7087
+ const parent = id.getParent();
7088
+ const symbol = parent && Node.isShorthandPropertyAssignment(parent)
7089
+ ? checker.getShorthandAssignmentValueSymbol(parent.compilerNode)
7090
+ : id.getSymbol()?.compilerSymbol;
7091
+ return symbol === paramSymbol;
7092
+ });
7093
+ const from = 'a call into a function that builds the request body inside itself';
7094
+ let passedThrough = false;
7095
+ const built = [];
7096
+ for (const use of uses) {
7097
+ // carrick#2668: `data ? JSON.stringify(data) : undefined` tests the
7098
+ // argument before it sends it; the test is no body.
7099
+ if (this.inSendNothingTest(use, body))
7100
+ continue;
7101
+ const value = this.bodyValueAround(use, body);
7102
+ if (value === use) {
7103
+ passedThrough = true;
7104
+ continue;
7105
+ }
7106
+ if (value) {
7107
+ built.push(value);
7108
+ continue;
7109
+ }
7110
+ const ancestors = use.getAncestors();
7111
+ const insideLiteral = ancestors
7112
+ .slice(0, ancestors.indexOf(body))
7113
+ .some((a) => Node.isObjectLiteralExpression(a) || Node.isArrayLiteralExpression(a));
7114
+ if (insideLiteral) {
7115
+ return {
7116
+ kind: 'abstain',
7117
+ why: `the located argument goes into a value the called function builds and does not send as a body (${from})`,
7118
+ };
7119
+ }
7120
+ }
7121
+ if (built.length === 0)
7122
+ return { kind: 'none' };
7123
+ if (passedThrough) {
7124
+ return { kind: 'abstain', why: `the called function both sends the argument and builds a body from it (${from})` };
7125
+ }
7126
+ if (callee.getTypeParameters().length > 0) {
7127
+ return { kind: 'abstain', why: `the called function is generic, so the body it builds may be typed by its type parameters (${from})` };
7128
+ }
7129
+ const types = built.map((value) => this.sentValue(value));
7130
+ const texts = new Set(types.map((value) => typeText(value.getType(), value)));
7131
+ if (texts.size !== 1) {
7132
+ return { kind: 'abstain', why: `the called function builds more than one body from the argument (${from})` };
7133
+ }
7134
+ const at = types[0];
7135
+ const type = at.getType();
7136
+ if (type.isTypeParameter() || printedTypeHoldsTopType([...texts][0])) {
7137
+ return { kind: 'abstain', why: `the body the called function builds is not fully typed (${from})` };
7138
+ }
7139
+ return { kind: 'body_type', type, at, from };
7140
+ }
7141
+ /**
7142
+ * The request body value `use` is part of, inside `scope`: the value of a
7143
+ * body-named object member, or the argument at a body slot of a call,
7144
+ * looking through parentheses, casts, `await` and a `JSON.stringify`
7145
+ * around it. `use` itself when it is that value whole.
7146
+ */
7147
+ bodyValueAround(use, scope) {
7148
+ let current = use;
7149
+ while (current !== scope) {
7150
+ const parent = current.getParent();
7151
+ if (!parent)
7152
+ return undefined;
7153
+ if (Node.isShorthandPropertyAssignment(parent) &&
7154
+ current === use &&
7155
+ BODY_MEMBER_NAMES.has(parent.getName())) {
7156
+ return use;
7157
+ }
7158
+ if (Node.isPropertyAssignment(parent) &&
7159
+ parent.getInitializer() === current &&
7160
+ BODY_MEMBER_NAMES.has(parent.getName())) {
7161
+ return this.isSameValue(current, use) ? use : current;
7162
+ }
7163
+ if (Node.isCallExpression(parent)) {
7164
+ const index = parent.getArguments().indexOf(current);
7165
+ if (index >= 0 && this.requestBodySlots(parent)[index]?.kind === 'body') {
7166
+ return this.isSameValue(current, use) ? use : current;
7167
+ }
7168
+ }
7169
+ current = parent;
7170
+ }
7171
+ return undefined;
7172
+ }
7173
+ /** `value` is `use` behind what `sentValue` sees through. */
7174
+ isSameValue(value, use) {
7175
+ return this.sentValue(value) === use;
7176
+ }
7177
+ /**
7178
+ * The value a body slot sends when it sends anything, behind parentheses,
7179
+ * casts, `await`, a `JSON.stringify`, and (carrick#2668) a choice whose
7180
+ * other side sends nothing: `x ? JSON.stringify(x) : undefined`,
7181
+ * `x || null`, `x ?? undefined`. A `?? {}` sends `{}`, so it is a body the
7182
+ * function builds and is not seen through.
7183
+ */
7184
+ sentValue(value) {
7185
+ let current = value;
7186
+ for (;;) {
7187
+ let inner = this.unwrapJsonStringifyArg(this.unwrapExpressionNode(current));
7188
+ if (Node.isConditionalExpression(inner)) {
7189
+ inner = this.sendingBranch(inner) ?? inner;
7190
+ }
7191
+ else if (Node.isBinaryExpression(inner)) {
7192
+ const operator = inner.getOperatorToken().getKind();
7193
+ if ((operator === SyntaxKind.BarBarToken || operator === SyntaxKind.QuestionQuestionToken) &&
7194
+ this.sendsNothing(inner.getRight(), false)) {
7195
+ inner = inner.getLeft();
7196
+ }
7197
+ }
7198
+ if (inner === current)
7199
+ return current;
7200
+ current = inner;
7201
+ }
7202
+ }
7203
+ /**
7204
+ * The branch of `choice` that sends something, when its other branch sends
7205
+ * nothing (`undefined`, `null`, `void 0`, or `{}` spread into a request).
7206
+ */
7207
+ sendingBranch(choice) {
7208
+ const whenTrue = choice.getWhenTrue();
7209
+ const whenFalse = choice.getWhenFalse();
7210
+ if (this.sendsNothing(whenFalse, true) && !this.sendsNothing(whenTrue, true))
7211
+ return whenTrue;
7212
+ if (this.sendsNothing(whenTrue, true) && !this.sendsNothing(whenFalse, true))
7213
+ return whenFalse;
7214
+ return undefined;
7215
+ }
7216
+ /** `undefined`, `null`, `void 0`, or (where `emptyObject`) `{}`. */
7217
+ sendsNothing(node, emptyObject) {
7218
+ const value = this.unwrapExpressionNode(node);
7219
+ if (Node.isNullLiteral(value) || Node.isVoidExpression(value))
7220
+ return true;
7221
+ if (Node.isIdentifier(value) && value.getText() === 'undefined')
7222
+ return true;
7223
+ return (emptyObject &&
7224
+ Node.isObjectLiteralExpression(value) &&
7225
+ value.getProperties().length === 0);
7226
+ }
7227
+ /**
7228
+ * carrick#2668, #2664: `use` sits in the test of a choice whose other side
7229
+ * sends nothing (`body ? JSON.stringify(body) : null`,
7230
+ * `...(body === undefined ? {} : { body: JSON.stringify(body) })`). The
7231
+ * test only decides whether the argument is sent.
7232
+ */
7233
+ inSendNothingTest(use, scope) {
7234
+ let current = use;
7235
+ while (current !== scope) {
7236
+ const parent = current.getParent();
7237
+ if (!parent)
7238
+ return false;
7239
+ if (Node.isConditionalExpression(parent) &&
7240
+ parent.getCondition() === current &&
7241
+ this.sendingBranch(parent)) {
7242
+ return true;
7243
+ }
7244
+ current = parent;
7245
+ }
7246
+ return false;
7247
+ }
6924
7248
  /** A string, a string literal, a template literal, or a union of strings. */
6925
7249
  isStringLikeType(type) {
6926
7250
  return (type.isString() ||
@@ -7285,71 +7609,304 @@ export class TypeInferrer {
7285
7609
  return values;
7286
7610
  }
7287
7611
  /**
7288
- * The contract of the schema a handler validates an untyped body read with
7289
- * (carrick#1166), or null.
7290
- *
7291
- * `read` is the located expression: the read call, or the binding that holds
7292
- * it. Every call in the enclosing function that takes that value as an
7293
- * argument is a candidate, and the schema is the call's receiver
7294
- * (`Schema.safeParse(body)`) or one of its other arguments
7295
- * (`parse(Schema, body)`). A candidate counts only when it declares schema
7296
- * types (`schemaOutputType` / `schemaInputType`), so a logger or a service
7297
- * call the body is also handed to is never read as a contract. No method or
7298
- * library name is matched.
7299
- */
7300
- schemaConsumingRead(read) {
7301
- const func = this.findContainingFunctionForNode(read);
7302
- if (!func)
7303
- return null;
7304
- const bindings = new Set();
7305
- const addBinding = (identifier) => {
7306
- const symbol = identifier?.getSymbol()?.compilerSymbol;
7307
- if (symbol)
7308
- bindings.add(symbol);
7612
+ * The type the function a value is made in states for it, where the
7613
+ * compiler types the value itself `any` or `unknown` (carrick#2594), or
7614
+ * null.
7615
+ *
7616
+ * `value` is the located expression: a body read, or a call whose result is
7617
+ * untyped. The value is followed through the names that hold it: a
7618
+ * declaration it initialises, and a name it is assigned to later
7619
+ * (`let body: unknown; body = await read()`). At each place the value or
7620
+ * one of those names is used whole, the source can state a type:
7621
+ *
7622
+ * 0. the annotation of the name that holds it (`let body: Upload`), or the
7623
+ * declared return type of the function that returns it unchanged (only
7624
+ * with `reads.returned`);
7625
+ * 1. a schema it is handed to (`Schema.safeParse(body ?? {})`,
7626
+ * `parse(Schema, body)`): the schema's input is what a sender may send
7627
+ * (carrick#1166). A candidate counts only when it declares schema types,
7628
+ * so no method or library name is matched;
7629
+ * 2. a function of the project it is handed to that returns that argument
7630
+ * unchanged (through casts) or an empty default: its declared return
7631
+ * type. A function that builds a new value from its argument is a
7632
+ * converter, and its return type describes what it built, never the
7633
+ * value it was handed, so it states nothing here;
7634
+ * 3. a cast of the whole value (`(body as { phone: string }).phone`);
7635
+ * 4. the parameter of a call it is handed to (only with
7636
+ * `reads.parameters`), where that parameter's type is a body shape once
7637
+ * any callable member is dropped (a function never crosses the wire).
7638
+ *
7639
+ * The first rank with an answer decides. A placeholder (`unknown`,
7640
+ * `Record<string, unknown>`, any type node with `any` or `unknown` written
7641
+ * in it) states nothing, and two different statements in one rank decide
7642
+ * nothing: the answer is null.
7643
+ */
7644
+ statedInFunction(value, func, reads) {
7645
+ const holders = new Set();
7646
+ const positions = [value];
7647
+ const holderUse = (node) => {
7648
+ if (!Node.isIdentifier(node))
7649
+ return false;
7650
+ const symbol = node.getSymbol()?.compilerSymbol;
7651
+ return !!symbol && holders.has(symbol);
7309
7652
  };
7310
- if (Node.isIdentifier(read)) {
7311
- addBinding(read);
7312
- }
7313
- else {
7314
- const declaration = read.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
7315
- const initializer = declaration?.getInitializer();
7316
- if (declaration &&
7317
- initializer &&
7318
- this.unwrapExpressionNode(initializer) === read &&
7319
- Node.isIdentifier(declaration.getNameNode())) {
7320
- addBinding(declaration.getNameNode());
7321
- }
7322
- }
7323
- const carriesRead = (argument) => argument === read ||
7324
- (Node.isIdentifier(argument) &&
7325
- bindings.has(argument.getSymbol()?.compilerSymbol));
7326
- for (const call of func.getDescendantsOfKind(SyntaxKind.CallExpression)) {
7327
- const args = call.getArguments().map((arg) => this.unwrapExpressionNode(arg));
7328
- const carrier = args.findIndex(carriesRead);
7329
- if (carrier < 0)
7653
+ const hold = (name) => {
7654
+ if (!Node.isIdentifier(name))
7655
+ return;
7656
+ const symbol = name.getSymbol()?.compilerSymbol;
7657
+ if (!symbol || holders.has(symbol))
7658
+ return;
7659
+ holders.add(symbol);
7660
+ for (const id of func.getDescendantsOfKind(SyntaxKind.Identifier)) {
7661
+ if (id === name || id.getSymbol()?.compilerSymbol !== symbol)
7662
+ continue;
7663
+ const parent = id.getParent();
7664
+ if (Node.isVariableDeclaration(parent) && parent.getNameNode() === id)
7665
+ continue;
7666
+ if (Node.isBinaryExpression(parent) &&
7667
+ parent.getLeft() === id &&
7668
+ parent.getOperatorToken().getKind() === SyntaxKind.EqualsToken) {
7669
+ continue;
7670
+ }
7671
+ positions.push(id);
7672
+ }
7673
+ };
7674
+ hold(value);
7675
+ const found = [];
7676
+ const stateNode = (rank, typeNode, at, how) => {
7677
+ if (!typeNode || this.leavesAPositionOpen(typeNode))
7678
+ return;
7679
+ const text = this.structuralTextFromTypeNode(typeNode);
7680
+ if (text)
7681
+ found.push({ rank, text, at, how });
7682
+ };
7683
+ for (let i = 0; i < positions.length; i++) {
7684
+ const position = positions[i];
7685
+ if (reads.skipFailurePaths &&
7686
+ position !== value &&
7687
+ reachedOnlyOnFailure(position, func, holderUse)) {
7330
7688
  continue;
7331
- const candidates = [];
7332
- const callee = call.getExpression();
7333
- if (Node.isPropertyAccessExpression(callee)) {
7334
- candidates.push(callee.getExpression());
7335
7689
  }
7336
- args.forEach((arg, index) => {
7337
- if (index !== carrier)
7338
- candidates.push(arg);
7339
- });
7340
- for (const candidate of candidates) {
7341
- const candidateType = candidate.getType();
7342
- if (!this.schemaOutputType(candidateType, candidate) &&
7343
- !this.schemaInputType(candidateType, candidate)) {
7690
+ // Climb the wrappers that leave the value what it was: parentheses,
7691
+ // `await`, `!`, `satisfies`, a placeholder cast, and an empty default
7692
+ // (`body ?? {}`).
7693
+ let outer = position;
7694
+ for (;;) {
7695
+ const parent = outer.getParent();
7696
+ if (!parent)
7697
+ break;
7698
+ if ((Node.isParenthesizedExpression(parent) ||
7699
+ Node.isAwaitExpression(parent) ||
7700
+ Node.isNonNullExpression(parent) ||
7701
+ Node.isSatisfiesExpression(parent)) &&
7702
+ parent.getExpression() === outer) {
7703
+ outer = parent;
7704
+ continue;
7705
+ }
7706
+ if ((Node.isAsExpression(parent) || Node.isTypeAssertion(parent)) &&
7707
+ parent.getExpression() === outer) {
7708
+ const typeNode = parent.getTypeNode();
7709
+ if (typeNode && !this.leavesAPositionOpen(typeNode)) {
7710
+ stateNode(3, typeNode, parent, 'a cast of the whole value');
7711
+ break;
7712
+ }
7713
+ outer = parent;
7714
+ continue;
7715
+ }
7716
+ if (Node.isBinaryExpression(parent) &&
7717
+ parent.getLeft() === outer &&
7718
+ [SyntaxKind.QuestionQuestionToken, SyntaxKind.BarBarToken].includes(parent.getOperatorToken().getKind()) &&
7719
+ this.isEmptyDefault(parent.getRight())) {
7720
+ outer = parent;
7721
+ continue;
7722
+ }
7723
+ break;
7724
+ }
7725
+ const parent = outer.getParent();
7726
+ if (!parent)
7727
+ continue;
7728
+ if (Node.isVariableDeclaration(parent) && parent.getInitializer() === outer) {
7729
+ stateNode(0, parent.getTypeNode(), parent, 'the annotation of the name that holds it');
7730
+ hold(parent.getNameNode());
7731
+ continue;
7732
+ }
7733
+ if (Node.isBinaryExpression(parent) &&
7734
+ parent.getRight() === outer &&
7735
+ parent.getOperatorToken().getKind() === SyntaxKind.EqualsToken &&
7736
+ Node.isIdentifier(parent.getLeft())) {
7737
+ const declaration = parent.getLeft().getSymbol()?.getValueDeclaration();
7738
+ if (declaration && Node.isVariableDeclaration(declaration)) {
7739
+ stateNode(0, declaration.getTypeNode(), declaration, 'the annotation of the name it is assigned to');
7740
+ }
7741
+ hold(parent.getLeft());
7742
+ continue;
7743
+ }
7744
+ if (reads.returned) {
7745
+ const returnedHere = (Node.isReturnStatement(parent) &&
7746
+ parent.getExpression() === outer &&
7747
+ this.findContainingFunctionForNode(parent) === func) ||
7748
+ (Node.isArrowFunction(parent) && parent === func && parent.getBody() === outer);
7749
+ if (returnedHere) {
7750
+ stateNode(0, func.getReturnTypeNode(), func, 'the declared return type of the function that returns it');
7344
7751
  continue;
7345
7752
  }
7346
- const contract = this.schemaContract(candidate, 'input');
7347
- if (contract)
7348
- return contract;
7349
7753
  }
7754
+ if (Node.isCallExpression(parent) && parent.getArguments().includes(outer)) {
7755
+ this.stateAtReceivingCall(parent, parent.getArguments().indexOf(outer), reads, found, stateNode);
7756
+ }
7757
+ }
7758
+ if (found.length === 0)
7759
+ return null;
7760
+ const rank = Math.min(...found.map((entry) => entry.rank));
7761
+ const decided = found.filter((entry) => entry.rank === rank);
7762
+ if (new Set(decided.map((entry) => entry.text)).size !== 1) {
7763
+ this.log(`A value at ${this.getNodeLocation(value).file_path}:${this.getNodeLocation(value).start_line} ` +
7764
+ `is stated as ${decided.length} different types in its function; stating none`);
7765
+ return null;
7766
+ }
7767
+ const [first] = decided;
7768
+ return { text: first.text, provenance: first.provenance, at: first.at, how: first.how };
7769
+ }
7770
+ /**
7771
+ * What a call that takes the value at argument `index` states for it:
7772
+ * a schema among its receiver and other arguments (rank 1), a callee of the
7773
+ * project that returns that argument unchanged (rank 2), or, with
7774
+ * `reads.parameters`, the parameter's own type (rank 4).
7775
+ */
7776
+ stateAtReceivingCall(call, index, reads, found, stateNode) {
7777
+ const contract = this.schemaHandedArgument(call, index);
7778
+ if (contract) {
7779
+ found.push({ rank: 1, text: contract.text, provenance: contract.provenance, at: call, how: 'a schema it is handed to' });
7780
+ return;
7781
+ }
7782
+ const passThrough = this.passThroughFunction(call, index);
7783
+ if (passThrough) {
7784
+ stateNode(2, passThrough.getReturnTypeNode(), call, 'a function that returns it unchanged');
7785
+ return;
7786
+ }
7787
+ if (!reads.parameters)
7788
+ return;
7789
+ const signature = this.project.getTypeChecker().getResolvedSignature(call);
7790
+ const parameter = signature?.getParameters()[index];
7791
+ const declaration = parameter?.getValueDeclaration();
7792
+ if (!parameter || (declaration && Node.isParameterDeclaration(declaration) && declaration.isRestParameter())) {
7793
+ return;
7794
+ }
7795
+ let parameterType = parameter.getTypeAtLocation(call);
7796
+ if (parameterType.isUnion()) {
7797
+ const bodies = parameterType.getUnionTypes().filter((part) => !this.isCallableType(part));
7798
+ if (bodies.length !== 1)
7799
+ return;
7800
+ parameterType = bodies[0];
7801
+ }
7802
+ if (!this.isObjectShape(parameterType))
7803
+ return;
7804
+ const text = this.structuralTextFromType(parameterType, call);
7805
+ if (!text || /\b(any|unknown)\b/.test(text))
7806
+ return;
7807
+ found.push({ rank: 4, text, at: call, how: 'the parameter of a call it is handed to' });
7808
+ }
7809
+ /**
7810
+ * The input contract of a schema `call` validates its argument at `index`
7811
+ * with: its receiver (`Schema.safeParse(body)`) or one of its other
7812
+ * arguments (`parse(Schema, body)`), or null. A candidate counts only when
7813
+ * it declares schema types, so no method or library name is matched
7814
+ * (carrick#1166).
7815
+ */
7816
+ schemaHandedArgument(call, index) {
7817
+ const candidates = [];
7818
+ const callee = call.getExpression();
7819
+ if (Node.isPropertyAccessExpression(callee))
7820
+ candidates.push(callee.getExpression());
7821
+ call.getArguments().forEach((argument, at) => {
7822
+ if (at !== index)
7823
+ candidates.push(this.unwrapExpressionNode(argument));
7824
+ });
7825
+ for (const candidate of candidates) {
7826
+ const candidateType = candidate.getType();
7827
+ if (!this.schemaOutputType(candidateType, candidate) &&
7828
+ !this.schemaInputType(candidateType, candidate)) {
7829
+ continue;
7830
+ }
7831
+ const contract = this.schemaContract(candidate, 'input');
7832
+ if (contract)
7833
+ return contract;
7350
7834
  }
7351
7835
  return null;
7352
7836
  }
7837
+ /**
7838
+ * `call`'s result describes the value it takes at argument `index`: it
7839
+ * validates it with a schema, or its callee returns it unchanged
7840
+ * (`passThroughFunction`). Any other call builds something new from the
7841
+ * value, and its result describes what it built, never the value it was
7842
+ * handed (carrick#2595).
7843
+ */
7844
+ callStatesItsArgument(call, index) {
7845
+ return index >= 0 && (!!this.schemaHandedArgument(call, index) || !!this.passThroughFunction(call, index));
7846
+ }
7847
+ /**
7848
+ * The project function `call` invokes, when every value it returns is its
7849
+ * argument at `index` as it was handed in (through parentheses, casts,
7850
+ * `satisfies` and `!`) or an empty default (`{}`, `[]`, `null`,
7851
+ * `undefined`), and at least one return is that argument. Such a function
7852
+ * narrows its argument, so its declared return type describes the value it
7853
+ * was handed. Any other function builds something new from it, and is not
7854
+ * this.
7855
+ */
7856
+ passThroughFunction(call, index) {
7857
+ let symbol = call.getExpression().getSymbol();
7858
+ if (symbol?.isAlias())
7859
+ symbol = symbol.getAliasedSymbol() ?? symbol;
7860
+ for (const declaration of symbol?.getDeclarations() ?? []) {
7861
+ const fn = Node.isFunctionDeclaration(declaration) || Node.isMethodDeclaration(declaration)
7862
+ ? declaration
7863
+ : Node.isVariableDeclaration(declaration)
7864
+ ? this.unwrapExpressionNode(declaration.getInitializer())
7865
+ : undefined;
7866
+ if (!fn ||
7867
+ !(Node.isFunctionDeclaration(fn) ||
7868
+ Node.isMethodDeclaration(fn) ||
7869
+ Node.isArrowFunction(fn) ||
7870
+ Node.isFunctionExpression(fn)) ||
7871
+ !fn.getBody() ||
7872
+ fn.getSourceFile().isDeclarationFile() ||
7873
+ fn.getSourceFile().isInNodeModules()) {
7874
+ continue;
7875
+ }
7876
+ const parameter = fn.getParameters()[index];
7877
+ const parameterSymbol = parameter?.getNameNode().getSymbol()?.compilerSymbol;
7878
+ if (!parameter || parameter.isRestParameter() || !parameterSymbol)
7879
+ continue;
7880
+ const returned = this.returnedExpressions(fn);
7881
+ let returnsArgument = false;
7882
+ const everyReturnPasses = returned.every((expression) => {
7883
+ let node = expression;
7884
+ while (Node.isParenthesizedExpression(node) ||
7885
+ Node.isAsExpression(node) ||
7886
+ Node.isTypeAssertion(node) ||
7887
+ Node.isSatisfiesExpression(node) ||
7888
+ Node.isNonNullExpression(node)) {
7889
+ node = node.getExpression();
7890
+ }
7891
+ if (Node.isIdentifier(node) && node.getSymbol()?.compilerSymbol === parameterSymbol) {
7892
+ returnsArgument = true;
7893
+ return true;
7894
+ }
7895
+ return this.isEmptyDefault(node);
7896
+ });
7897
+ if (everyReturnPasses && returnsArgument)
7898
+ return fn;
7899
+ }
7900
+ return undefined;
7901
+ }
7902
+ /** `{}`, `[]`, `null` or `undefined`: a default that states no member. */
7903
+ isEmptyDefault(node) {
7904
+ const bare = this.unwrapExpressionNode(node);
7905
+ return ((Node.isObjectLiteralExpression(bare) && bare.getProperties().length === 0) ||
7906
+ (Node.isArrayLiteralExpression(bare) && bare.getElements().length === 0) ||
7907
+ Node.isNullLiteral(bare) ||
7908
+ (Node.isIdentifier(bare) && bare.getText() === 'undefined'));
7909
+ }
7353
7910
  /**
7354
7911
  * The part a validated read names when that part is not a body, or
7355
7912
  * `undefined` (carrick#1166).