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