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.
- 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/check-classify.js +11 -5
- package/sidecar/dist/src/capture/check.js +8 -1
- package/sidecar/dist/src/capture/deep-walk.d.ts +31 -0
- package/sidecar/dist/src/capture/deep-walk.js +43 -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/capture/index.d.ts +1 -1
- package/sidecar/dist/src/capture/index.js +1 -1
- package/sidecar/dist/src/capture/service-config.d.ts +11 -0
- package/sidecar/dist/src/capture/service-config.js +40 -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 +35 -1
- package/sidecar/dist/src/library-claims.d.ts +17 -2
- package/sidecar/dist/src/library-claims.js +242 -47
- 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 +31 -0
- package/sidecar/dist/src/project-loader.js +67 -4
- package/sidecar/dist/src/type-inferrer.d.ts +150 -16
- package/sidecar/dist/src/type-inferrer.js +651 -94
- package/sidecar/dist/src/types.d.ts +46 -2
- package/sidecar/dist/src/validators.d.ts +73 -0
- 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
|
-
|
|
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
|
|
2663
|
-
// `Schema.safeParse(body)`,
|
|
2664
|
-
//
|
|
2665
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
2750
|
-
//
|
|
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
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
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
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
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
|
-
|
|
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
|
|
6610
|
-
* (`
|
|
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
|
|
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
|
-
|
|
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
|
|
7289
|
-
* (carrick#
|
|
7290
|
-
*
|
|
7291
|
-
*
|
|
7292
|
-
*
|
|
7293
|
-
*
|
|
7294
|
-
*
|
|
7295
|
-
* (`
|
|
7296
|
-
*
|
|
7297
|
-
*
|
|
7298
|
-
*
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
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
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
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
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
for (
|
|
7341
|
-
const
|
|
7342
|
-
if (!
|
|
7343
|
-
|
|
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).
|