carrick 0.3.127 → 0.3.129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/anchors.js +5 -1
- package/sidecar/dist/src/capture/check-classify.d.ts +8 -1
- package/sidecar/dist/src/capture/check-classify.js +21 -1
- package/sidecar/dist/src/capture/check-probe.d.ts +7 -7
- package/sidecar/dist/src/capture/check-probe.js +13 -14
- package/sidecar/dist/src/capture/check.js +5 -5
- package/sidecar/dist/src/index.js +43 -4
- package/sidecar/dist/src/library-claims.d.ts +63 -3
- package/sidecar/dist/src/library-claims.js +380 -39
- package/sidecar/dist/src/retype.js +36 -0
- package/sidecar/dist/src/server-labels.d.ts +29 -0
- package/sidecar/dist/src/server-labels.js +45 -0
- package/sidecar/dist/src/stated-params.d.ts +138 -0
- package/sidecar/dist/src/stated-params.js +547 -0
- package/sidecar/dist/src/type-inferrer.d.ts +89 -1
- package/sidecar/dist/src/type-inferrer.js +429 -17
- package/sidecar/dist/src/types.d.ts +76 -3
- package/sidecar/dist/src/unwidened.d.ts +11 -2
- package/sidecar/dist/src/unwidened.js +50 -3
- package/sidecar/dist/src/validators.d.ts +228 -59
- package/sidecar/dist/src/validators.js +45 -2
|
@@ -31,6 +31,7 @@ import { functionAtLine } from './function-line-index.js';
|
|
|
31
31
|
import { attributedDeclaration, installedPackageOf } from './declaration-attribution.js';
|
|
32
32
|
import { endLineOf, startLineOf } from './line-index.js';
|
|
33
33
|
import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
|
|
34
|
+
import { StatedParams } from './stated-params.js';
|
|
34
35
|
import { classifyStatusCodes, constituents, httpStatus, statesNoBody, statesNoShape, statusKeyCode, statusTableOf, successEntries, } from './capture/index.js';
|
|
35
36
|
import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
|
|
36
37
|
import { expandTypeStructural, jsonWireType, } from './type-structural-expander.js';
|
|
@@ -187,6 +188,18 @@ const WRAPPER_CALL_MAX_DEPTH = 3;
|
|
|
187
188
|
* the branch an error path, whose shape is not the endpoint's contract.
|
|
188
189
|
*/
|
|
189
190
|
const STATUS_MEMBER_NAMES = ['status', 'statusCode'];
|
|
191
|
+
/**
|
|
192
|
+
* The reasons an `unknown` answer is a decision, not a failure to read: the
|
|
193
|
+
* list `DECIDED_ABSTAIN_REASONS` in `src/engine/type_compat_v2.rs` holds
|
|
194
|
+
* final. A stated reply never replaces one (carrick#2674).
|
|
195
|
+
*/
|
|
196
|
+
const DECIDED_ABSTAIN_REASONS = new Set([
|
|
197
|
+
'no_success_payload',
|
|
198
|
+
'no_request_body',
|
|
199
|
+
'projected_value_only',
|
|
200
|
+
'serialised_body_unread',
|
|
201
|
+
'body_built_by_callee',
|
|
202
|
+
]);
|
|
190
203
|
/** The provenance sentence for each reason a branch was not read. */
|
|
191
204
|
const SERIALISED_BODY_UNREAD_DETAIL = {
|
|
192
205
|
prebuilt: 'A return hands back a status and body built somewhere else, such as a variable or a field of another result, so the value in its body was not read.',
|
|
@@ -448,13 +461,16 @@ export class TypeInferrer {
|
|
|
448
461
|
* @param onRequestDone - Called once per request, as the batch is done with it
|
|
449
462
|
* @returns InferResult with inferred types or errors
|
|
450
463
|
*/
|
|
451
|
-
infer(requests, extractionConfig, onRequestDone, modelSymbols) {
|
|
464
|
+
infer(requests, extractionConfig, onRequestDone, modelSymbols, labels) {
|
|
452
465
|
const started = performance.now();
|
|
453
466
|
this.modelSymbols = new Map((modelSymbols ?? []).map((symbol) => [symbol.alias, symbol]));
|
|
467
|
+
this.statedParams = this.statedParamsFor(labels ?? { sends: new Map(), bodies: new Set() });
|
|
454
468
|
const inferredTypes = [];
|
|
455
469
|
const errors = [];
|
|
456
470
|
/** Response inferences the unwidened reading re-reads (carrick#1516). */
|
|
457
471
|
const responses = [];
|
|
472
|
+
/** Request-body inferences it re-reads after them (carrick#2011). */
|
|
473
|
+
const bodies = [];
|
|
458
474
|
/** How long each request took, in the order they were done (carrick#1985). */
|
|
459
475
|
const timings = [];
|
|
460
476
|
for (const request of requests) {
|
|
@@ -491,6 +507,9 @@ export class TypeInferrer {
|
|
|
491
507
|
if (request.infer_kind === 'response_body' || request.infer_kind === 'function_return') {
|
|
492
508
|
responses.push({ request, result });
|
|
493
509
|
}
|
|
510
|
+
else if (request.infer_kind === 'request_body') {
|
|
511
|
+
bodies.push({ request, result });
|
|
512
|
+
}
|
|
494
513
|
}
|
|
495
514
|
else {
|
|
496
515
|
errors.push(`Could not infer type at ${request.file_path}:${loc} (${request.infer_kind})`);
|
|
@@ -521,10 +540,47 @@ export class TypeInferrer {
|
|
|
521
540
|
}
|
|
522
541
|
try {
|
|
523
542
|
const deadline = Math.min(performance.now() + this.unwidenedBudgetMs, started + UNWIDENED_LATEST_MS);
|
|
524
|
-
|
|
543
|
+
// The responses' rewrite forgets every node of the files it touches, so
|
|
544
|
+
// each body's read node is kept as its span, taken now and found again
|
|
545
|
+
// in the restored file.
|
|
546
|
+
const bodySpans = bodies.map((body) => {
|
|
547
|
+
const node = this.readNodes.get(body.result.source_location);
|
|
548
|
+
return {
|
|
549
|
+
...body,
|
|
550
|
+
readSpan: node && {
|
|
551
|
+
file: node.getSourceFile(),
|
|
552
|
+
start: node.getStart(),
|
|
553
|
+
end: node.getEnd(),
|
|
554
|
+
kind: node.getKind(),
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
});
|
|
558
|
+
try {
|
|
559
|
+
this.addUnwidenedReadings(responses, extractionConfig, deadline, 'response');
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
// The reading only ever adds a field; it never costs an answer.
|
|
563
|
+
this.logError(`Unwidened reading failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
// Second, and in a rewrite of its own: the bodies' files never delay a
|
|
567
|
+
// response's reading, whose budget a spent deadline turns into an
|
|
568
|
+
// abstention (carrick#2100).
|
|
569
|
+
this.addUnwidenedReadings(bodySpans, extractionConfig, deadline, 'request_body');
|
|
570
|
+
}
|
|
571
|
+
catch (err) {
|
|
572
|
+
this.logError(`Unwidened reading of request bodies failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
573
|
+
// carrick#2011: a body whose reading was not made is judged on its
|
|
574
|
+
// widened type alone, so it is marked the way a spent budget marks it
|
|
575
|
+
// and its pair abstains on a mismatch rather than report one.
|
|
576
|
+
for (const body of bodies) {
|
|
577
|
+
if (body.result.unwidened_type_string === undefined) {
|
|
578
|
+
body.result.unwidened_out_of_budget = true;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
525
582
|
}
|
|
526
583
|
catch (err) {
|
|
527
|
-
// The reading only ever adds a field; it never costs an answer.
|
|
528
584
|
this.logError(`Unwidened reading failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
529
585
|
}
|
|
530
586
|
return {
|
|
@@ -536,6 +592,12 @@ export class TypeInferrer {
|
|
|
536
592
|
}
|
|
537
593
|
/** The types the model named, by the alias of the row each is for (carrick#2111). */
|
|
538
594
|
modelSymbols = new Map();
|
|
595
|
+
/**
|
|
596
|
+
* carrick#2674: what route handlers' parameters state, through the
|
|
597
|
+
* members the batch's verified server-framework labels name. Empty when
|
|
598
|
+
* the batch carries no labels, and then it reads nothing.
|
|
599
|
+
*/
|
|
600
|
+
statedParams = this.statedParamsFor({ sends: new Map(), bodies: new Set() });
|
|
539
601
|
/**
|
|
540
602
|
* carrick#2111: where the model named a type for the response this request
|
|
541
603
|
* reads, record on `inferred` how that type relates to the compiler's type
|
|
@@ -620,6 +682,10 @@ export class TypeInferrer {
|
|
|
620
682
|
* really returns beside the published one (`unwidened_type_string`). See
|
|
621
683
|
* `unwidened.ts` for what is marked and why the reading is sound.
|
|
622
684
|
*
|
|
685
|
+
* carrick#2011: a request body (`read` is `'request_body'`) is read the same
|
|
686
|
+
* way, the call that sends it in place of the handler, except that a literal
|
|
687
|
+
* handed to any call other than the send is never marked.
|
|
688
|
+
*
|
|
623
689
|
* One rewrite for the whole batch: every file on any request's path is
|
|
624
690
|
* rewritten once, type-checked before and after, the requests re-inferred,
|
|
625
691
|
* and every file restored before returning, so the project other requests
|
|
@@ -631,7 +697,7 @@ export class TypeInferrer {
|
|
|
631
697
|
* reading (a declared return, nothing to mark) or lost it for a reason of
|
|
632
698
|
* its own (a marking that added a diagnostic) is never marked.
|
|
633
699
|
*/
|
|
634
|
-
addUnwidenedReadings(responses, extractionConfig, deadline) {
|
|
700
|
+
addUnwidenedReadings(responses, extractionConfig, deadline, read) {
|
|
635
701
|
const outOfTime = (stage, unread) => {
|
|
636
702
|
if (performance.now() <= deadline)
|
|
637
703
|
return false;
|
|
@@ -646,17 +712,19 @@ export class TypeInferrer {
|
|
|
646
712
|
const checkedFiles = new Set();
|
|
647
713
|
// Every position is read before the first rewrite: a rewrite forgets
|
|
648
714
|
// every node of its file.
|
|
649
|
-
for (const { request, result } of responses) {
|
|
715
|
+
for (const { request, result, readSpan } of responses) {
|
|
650
716
|
// A declared return type is the contract; nothing was widened.
|
|
651
717
|
if (result.is_explicit)
|
|
652
718
|
continue;
|
|
653
|
-
const
|
|
654
|
-
|
|
719
|
+
const readNode = readSpan
|
|
720
|
+
? nodeAtSpan(readSpan)
|
|
721
|
+
: this.readNodes.get(result.source_location);
|
|
722
|
+
if (!readNode)
|
|
655
723
|
continue;
|
|
656
|
-
const functions = pathOf(
|
|
724
|
+
const functions = pathOf(readNode);
|
|
657
725
|
let marked = 0;
|
|
658
726
|
for (const fn of functions) {
|
|
659
|
-
const insertions = literalInsertions(fn);
|
|
727
|
+
const insertions = literalInsertions(fn, read === 'request_body' ? readNode : undefined);
|
|
660
728
|
marked += insertions.length;
|
|
661
729
|
if (insertions.length === 0)
|
|
662
730
|
continue;
|
|
@@ -685,7 +753,7 @@ export class TypeInferrer {
|
|
|
685
753
|
plans.push({
|
|
686
754
|
request,
|
|
687
755
|
result,
|
|
688
|
-
read: { file:
|
|
756
|
+
read: { file: readNode.getSourceFile(), start: readNode.getStart(), end: readNode.getEnd() },
|
|
689
757
|
path,
|
|
690
758
|
locatedByText,
|
|
691
759
|
});
|
|
@@ -807,7 +875,7 @@ export class TypeInferrer {
|
|
|
807
875
|
case 'function_return':
|
|
808
876
|
return this.inferFunctionReturn(sourceFile, request, extractionConfig);
|
|
809
877
|
case 'response_body':
|
|
810
|
-
return this.inferResponseBody(sourceFile, request, extractionConfig);
|
|
878
|
+
return this.withStatedReply(sourceFile, request, this.inferResponseBody(sourceFile, request, extractionConfig));
|
|
811
879
|
case 'call_result':
|
|
812
880
|
return this.inferCallResult(sourceFile, request, extractionConfig);
|
|
813
881
|
case 'variable':
|
|
@@ -1909,6 +1977,16 @@ export class TypeInferrer {
|
|
|
1909
1977
|
typeString = explicitType;
|
|
1910
1978
|
isExplicit = true;
|
|
1911
1979
|
}
|
|
1980
|
+
// carrick#2714: the body read is cast to a placeholder
|
|
1981
|
+
// (`(await res.json()) as { data: Item[] } | { error: unknown }`) and the
|
|
1982
|
+
// source narrows the value later (`return json as { data: Item[] }`).
|
|
1983
|
+
// The narrowed statement is the body, and it is reported as stated, so a
|
|
1984
|
+
// model symbol naming an element inside it is not kept as the body.
|
|
1985
|
+
const narrowed = func ? this.narrowedBodyStatement(terminalNode, func) : undefined;
|
|
1986
|
+
if (narrowed) {
|
|
1987
|
+
typeString = narrowed.text;
|
|
1988
|
+
isExplicit = true;
|
|
1989
|
+
}
|
|
1912
1990
|
// carrick#1843: a rule can now match a thenable or a carrier by its alias,
|
|
1913
1991
|
// so on a result that is a carrier (`Task<Outcome<Reply<T>, E>>`) the
|
|
1914
1992
|
// rules reach what the carrier holds before the carrier read below does.
|
|
@@ -1920,6 +1998,17 @@ export class TypeInferrer {
|
|
|
1920
1998
|
const callArgs = this.callResultArguments(callExpr);
|
|
1921
1999
|
const resultIsCarrier = !explicitType &&
|
|
1922
2000
|
this.resultCarrierArguments(returnType, terminalNode, callArgs) !== undefined;
|
|
2001
|
+
// carrick#2714, carrick#1841: the call returns a request library's
|
|
2002
|
+
// response object, the body in a member around a status, headers and the
|
|
2003
|
+
// request's config. The body is what the source reads that member as, and
|
|
2004
|
+
// never the object: judged against a producer's body, every transport
|
|
2005
|
+
// member reads as one the producer does not send.
|
|
2006
|
+
if (!explicitType && !resultIsCarrier && func) {
|
|
2007
|
+
const transport = this.transportRuleOfCall(callExpr, extractionConfig);
|
|
2008
|
+
if (transport && !this.callStatesTypedPayload(callExpr, extractionConfig)) {
|
|
2009
|
+
return this.transportCallBody(request, callExpr, func, transport.rule, where);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1923
2012
|
// carrick#1877: the terminal holds a thenable the names do not peel, a
|
|
1924
2013
|
// subclass of `Promise` or a class with a `then` of its own. What a
|
|
1925
2014
|
// caller receives is what `await` yields for it, as it is for the
|
|
@@ -2133,7 +2222,9 @@ export class TypeInferrer {
|
|
|
2133
2222
|
const leftCarrier = (callUnwrap.wasUnwrapped || readByProtocol) &&
|
|
2134
2223
|
!!callAfterRules &&
|
|
2135
2224
|
this.resultCarrierArguments(callAfterRules, callExpr, callArgs) !== undefined;
|
|
2136
|
-
const anchorSource =
|
|
2225
|
+
const anchorSource = narrowed?.typeNode
|
|
2226
|
+
? narrowed.typeNode.getType()
|
|
2227
|
+
: carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
|
|
2137
2228
|
let anchor = anchorSource
|
|
2138
2229
|
? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
|
|
2139
2230
|
: undefined;
|
|
@@ -2182,7 +2273,11 @@ export class TypeInferrer {
|
|
|
2182
2273
|
// carrick#1749: say when the text is the source's own statement of the
|
|
2183
2274
|
// body, and what that statement is rooted at, so the scanner can tell a
|
|
2184
2275
|
// model symbol that names the body from one that names a part of it.
|
|
2185
|
-
const statedBody =
|
|
2276
|
+
const statedBody = narrowed?.typeNode
|
|
2277
|
+
? this.statedRoot(narrowed.typeNode)
|
|
2278
|
+
: explicitType
|
|
2279
|
+
? this.statedBodyAtRead(terminalNode)
|
|
2280
|
+
: undefined;
|
|
2186
2281
|
if (statedBody) {
|
|
2187
2282
|
inferred.stated_body = statedBody;
|
|
2188
2283
|
}
|
|
@@ -3460,6 +3555,243 @@ export class TypeInferrer {
|
|
|
3460
3555
|
}
|
|
3461
3556
|
return common;
|
|
3462
3557
|
}
|
|
3558
|
+
/**
|
|
3559
|
+
* What the source narrows a call's value to, where the type stated at the
|
|
3560
|
+
* walk's terminal is a placeholder that leaves a position open
|
|
3561
|
+
* (carrick#2714, `as { data: Item[] } | { error: unknown }`): the one type
|
|
3562
|
+
* stated for the value later in the function, on its success path
|
|
3563
|
+
* (`return json as { data: Item[] }`). A closed statement at the terminal
|
|
3564
|
+
* keeps the answer it had.
|
|
3565
|
+
*/
|
|
3566
|
+
narrowedBodyStatement(terminal, func) {
|
|
3567
|
+
const placeholder = this.explicitTypeNodeFromAncestor(terminal);
|
|
3568
|
+
if (!placeholder || !this.leavesAPositionOpen(placeholder))
|
|
3569
|
+
return undefined;
|
|
3570
|
+
const stated = this.statedInFunction(terminal, func, {
|
|
3571
|
+
returned: true,
|
|
3572
|
+
parameters: false,
|
|
3573
|
+
skipFailurePaths: true,
|
|
3574
|
+
});
|
|
3575
|
+
return stated?.typeNode ? stated : undefined;
|
|
3576
|
+
}
|
|
3577
|
+
/**
|
|
3578
|
+
* The rule that verifies a call's result as a request library's response
|
|
3579
|
+
* object, or undefined (carrick#2714). The result is that object when its
|
|
3580
|
+
* awaited type is one a rule recognises. It is that object too when the
|
|
3581
|
+
* call states no type arguments and its declared result, with its free
|
|
3582
|
+
* type parameters at their defaults (`freeResultAtDefaults`), is a type a
|
|
3583
|
+
* rule recognises (`get<T, R = Response<T>>(): Promise<R>`).
|
|
3584
|
+
* The compiler then takes the parameter from where the value goes
|
|
3585
|
+
* (`return client.post(…)` in a function declared `Promise<void>` types the
|
|
3586
|
+
* call `Promise<void>`), and that is the caller's claim about the object,
|
|
3587
|
+
* not what the call returns.
|
|
3588
|
+
*/
|
|
3589
|
+
transportRuleOfCall(callExpr, extractionConfig) {
|
|
3590
|
+
if (!extractionConfig || extractionConfig.rules.length === 0)
|
|
3591
|
+
return undefined;
|
|
3592
|
+
const awaited = this.unwrapPromiseType(callExpr.getType());
|
|
3593
|
+
const direct = this.ruleRecognising(awaited, callExpr, extractionConfig);
|
|
3594
|
+
if (direct)
|
|
3595
|
+
return { rule: direct };
|
|
3596
|
+
if (callExpr.getTypeArguments().length > 0)
|
|
3597
|
+
return undefined;
|
|
3598
|
+
const declared = this.freeResultAtDefaults(callExpr);
|
|
3599
|
+
const rule = declared ? this.ruleRecognising(declared, callExpr, extractionConfig) : undefined;
|
|
3600
|
+
return rule ? { rule } : undefined;
|
|
3601
|
+
}
|
|
3602
|
+
/**
|
|
3603
|
+
* The first rule that matches `type` as a wrapper whose body is a member
|
|
3604
|
+
* (`payloadPropertyPath`) the type has, whether or not it reads a payload
|
|
3605
|
+
* out of it. A
|
|
3606
|
+
* wrapper with no such member (the platform's `Response`) is read by its
|
|
3607
|
+
* body reads (`res.json()`), which the def-use walk follows.
|
|
3608
|
+
*/
|
|
3609
|
+
ruleRecognising(type, node, config) {
|
|
3610
|
+
return config.rules.find((rule) => !!rule.payloadPropertyPath?.length &&
|
|
3611
|
+
!!type.getProperty(rule.payloadPropertyPath[0]) &&
|
|
3612
|
+
this.tryUnwrapWithRule(type, node, rule, config, 0).kind !== 'no-match');
|
|
3613
|
+
}
|
|
3614
|
+
/**
|
|
3615
|
+
* What a call's declared result is when its free type parameters take
|
|
3616
|
+
* their defaults, or undefined. A parameter is free when the declaration's
|
|
3617
|
+
* result mentions it and none of its parameters do, so only a type
|
|
3618
|
+
* argument, the default or the call's return position can fix it. The
|
|
3619
|
+
* result is read through `Promise`, a free parameter (its default), and a
|
|
3620
|
+
* conditional alias whose check type is a free parameter tested against
|
|
3621
|
+
* exactly that parameter's default (`R extends Default ? Response<T> : R`:
|
|
3622
|
+
* the true branch). Anything else is the type as written.
|
|
3623
|
+
*/
|
|
3624
|
+
freeResultAtDefaults(callExpr) {
|
|
3625
|
+
const declaration = this.project
|
|
3626
|
+
.getTypeChecker()
|
|
3627
|
+
.getResolvedSignature(callExpr)
|
|
3628
|
+
?.getDeclaration();
|
|
3629
|
+
if (!declaration ||
|
|
3630
|
+
!(Node.isMethodSignature(declaration) ||
|
|
3631
|
+
Node.isMethodDeclaration(declaration) ||
|
|
3632
|
+
Node.isFunctionDeclaration(declaration) ||
|
|
3633
|
+
Node.isCallSignatureDeclaration(declaration))) {
|
|
3634
|
+
return undefined;
|
|
3635
|
+
}
|
|
3636
|
+
const returnNode = declaration.getReturnTypeNode();
|
|
3637
|
+
if (!returnNode)
|
|
3638
|
+
return undefined;
|
|
3639
|
+
const referenced = (node) => new Set((node ? [node, ...node.getDescendants()] : [])
|
|
3640
|
+
.filter((n) => Node.isTypeReference(n))
|
|
3641
|
+
.map((n) => n.getTypeName().getText()));
|
|
3642
|
+
const inResult = referenced(returnNode);
|
|
3643
|
+
const inParameters = new Set(declaration.getParameters().flatMap((parameter) => [...referenced(parameter.getTypeNode())]));
|
|
3644
|
+
const free = new Map();
|
|
3645
|
+
for (const parameter of declaration.getTypeParameters()) {
|
|
3646
|
+
const name = parameter.getName();
|
|
3647
|
+
if (inResult.has(name) && !inParameters.has(name))
|
|
3648
|
+
free.set(name, parameter.getDefault());
|
|
3649
|
+
}
|
|
3650
|
+
if (free.size === 0)
|
|
3651
|
+
return undefined;
|
|
3652
|
+
const atDefaults = (node, depth) => {
|
|
3653
|
+
if (!node || depth > 4)
|
|
3654
|
+
return undefined;
|
|
3655
|
+
if (!Node.isTypeReference(node))
|
|
3656
|
+
return node.getType();
|
|
3657
|
+
const name = node.getTypeName().getText();
|
|
3658
|
+
if (free.has(name))
|
|
3659
|
+
return atDefaults(free.get(name), depth + 1);
|
|
3660
|
+
const args = node.getTypeArguments();
|
|
3661
|
+
if ((name === 'Promise' || name === 'PromiseLike') && args.length === 1) {
|
|
3662
|
+
return atDefaults(args[0], depth + 1);
|
|
3663
|
+
}
|
|
3664
|
+
const alias = node
|
|
3665
|
+
.getTypeName()
|
|
3666
|
+
.getSymbol()
|
|
3667
|
+
?.getDeclarations()
|
|
3668
|
+
.find((d) => Node.isTypeAliasDeclaration(d));
|
|
3669
|
+
const conditional = alias && Node.isTypeAliasDeclaration(alias) ? alias.getTypeNode() : undefined;
|
|
3670
|
+
if (alias && Node.isTypeAliasDeclaration(alias) && conditional && Node.isConditionalTypeNode(conditional)) {
|
|
3671
|
+
const index = alias
|
|
3672
|
+
.getTypeParameters()
|
|
3673
|
+
.findIndex((parameter) => parameter.getName() === conditional.getCheckType().getText());
|
|
3674
|
+
const argument = index >= 0 ? args[index] : undefined;
|
|
3675
|
+
const tested = argument && Node.isTypeReference(argument) ? argument.getTypeName().getText() : undefined;
|
|
3676
|
+
const defaulted = tested !== undefined ? free.get(tested) : undefined;
|
|
3677
|
+
if (defaulted && conditional.getExtendsType().getText() === defaulted.getText()) {
|
|
3678
|
+
return atDefaults(conditional.getTrueType(), depth + 1);
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
return node.getType();
|
|
3682
|
+
};
|
|
3683
|
+
return atDefaults(returnNode, 0);
|
|
3684
|
+
}
|
|
3685
|
+
/**
|
|
3686
|
+
* The call states the body itself: the rules read a payload out of its
|
|
3687
|
+
* result (`client.get<Widget>(url)`). An untyped payload is no payload to
|
|
3688
|
+
* the rules (they verify the object and read nothing), and a result its
|
|
3689
|
+
* return position chose is no response object to them.
|
|
3690
|
+
*/
|
|
3691
|
+
callStatesTypedPayload(callExpr, extractionConfig) {
|
|
3692
|
+
const unwrap = this.unwrapTypeWithConfig(this.unwrapPromiseType(callExpr.getType()), callExpr, extractionConfig);
|
|
3693
|
+
return unwrap.wasUnwrapped && !unwrap.verifiedMachinery && !!unwrap.payloadType;
|
|
3694
|
+
}
|
|
3695
|
+
/**
|
|
3696
|
+
* The body of a call that returns a response object (carrick#2714): the
|
|
3697
|
+
* one type the source states, in the function the call is made in, for the
|
|
3698
|
+
* reads of the object's payload member (`res.data as Widget`, `const w:
|
|
3699
|
+
* Widget = res.data`, `return res.data` from a function that declares its
|
|
3700
|
+
* return type), through a binding or in place. Where no read states one,
|
|
3701
|
+
* or two state different types, or the object is handed on whole, the
|
|
3702
|
+
* site states no response contract, and the abstain is decided so the
|
|
3703
|
+
* capture does not re-read the call and publish the object.
|
|
3704
|
+
*/
|
|
3705
|
+
transportCallBody(request, callExpr, func, rule, where) {
|
|
3706
|
+
const reads = this.payloadMemberReads(callExpr, func, rule.payloadPropertyPath ?? []);
|
|
3707
|
+
const statements = reads
|
|
3708
|
+
.map((read) => this.statedInFunction(read, func, { returned: true, parameters: true, skipFailurePaths: true }))
|
|
3709
|
+
.filter((stated) => stated !== null);
|
|
3710
|
+
const texts = new Set(statements.map((stated) => stated.text));
|
|
3711
|
+
if (texts.size !== 1) {
|
|
3712
|
+
this.log(`Call result at ${where} is a response object around the body, and the source states ` +
|
|
3713
|
+
`${texts.size === 0 ? 'no type' : `${texts.size} different types`} for the body it reads; ` +
|
|
3714
|
+
'this site states no response contract');
|
|
3715
|
+
return this.transportAbstain(request, callExpr, "this call returns the client's response object around the body, and the source " +
|
|
3716
|
+
'states no single type for the body it reads from it, so this site states no ' +
|
|
3717
|
+
'response contract');
|
|
3718
|
+
}
|
|
3719
|
+
const [stated] = statements;
|
|
3720
|
+
this.log(`Call result at ${where} is a response object; the source states its body (${stated.how})`);
|
|
3721
|
+
const anchor = stated.typeNode
|
|
3722
|
+
? this.unwrapArrayLevels(this.unwrapPromiseType(stated.typeNode.getType()))
|
|
3723
|
+
: undefined;
|
|
3724
|
+
const inferred = this.createInferredType(request, stated.text, true, this.getNodeLocation(stated.at), undefined, anchor ? this.primaryTypeSymbol(anchor.element) : undefined, anchor?.depth, anchor ? this.primaryTypeSymbolSource(anchor.element) : undefined);
|
|
3725
|
+
if (stated.typeNode) {
|
|
3726
|
+
inferred.stated_body = this.statedRoot(stated.typeNode);
|
|
3727
|
+
}
|
|
3728
|
+
return inferred;
|
|
3729
|
+
}
|
|
3730
|
+
/**
|
|
3731
|
+
* The reads of a response object's payload member (`path`, e.g. `data`)
|
|
3732
|
+
* that take it off the call's value: in place (`(await call).data`),
|
|
3733
|
+
* through the name the value is bound to (`res.data`), or as a name
|
|
3734
|
+
* destructured out of it (`const { data } = await call`).
|
|
3735
|
+
*/
|
|
3736
|
+
payloadMemberReads(callExpr, func, path) {
|
|
3737
|
+
if (path.length === 0)
|
|
3738
|
+
return [];
|
|
3739
|
+
const follow = (start) => {
|
|
3740
|
+
let node = start;
|
|
3741
|
+
for (const segment of path) {
|
|
3742
|
+
const parent = node.getParent();
|
|
3743
|
+
if (!parent ||
|
|
3744
|
+
!Node.isPropertyAccessExpression(parent) ||
|
|
3745
|
+
parent.getExpression() !== node ||
|
|
3746
|
+
parent.getName() !== segment) {
|
|
3747
|
+
return undefined;
|
|
3748
|
+
}
|
|
3749
|
+
node = parent;
|
|
3750
|
+
}
|
|
3751
|
+
return node;
|
|
3752
|
+
};
|
|
3753
|
+
let value = callExpr;
|
|
3754
|
+
for (;;) {
|
|
3755
|
+
const parent = value.getParent();
|
|
3756
|
+
if (parent &&
|
|
3757
|
+
(Node.isParenthesizedExpression(parent) ||
|
|
3758
|
+
Node.isAwaitExpression(parent) ||
|
|
3759
|
+
Node.isNonNullExpression(parent)) &&
|
|
3760
|
+
parent.getExpression() === value) {
|
|
3761
|
+
value = parent;
|
|
3762
|
+
continue;
|
|
3763
|
+
}
|
|
3764
|
+
break;
|
|
3765
|
+
}
|
|
3766
|
+
const inPlace = follow(value);
|
|
3767
|
+
if (inPlace)
|
|
3768
|
+
return [inPlace];
|
|
3769
|
+
const declaration = value.getParent();
|
|
3770
|
+
if (!declaration || !Node.isVariableDeclaration(declaration) || declaration.getInitializer() !== value) {
|
|
3771
|
+
return [];
|
|
3772
|
+
}
|
|
3773
|
+
const name = declaration.getNameNode();
|
|
3774
|
+
if (Node.isObjectBindingPattern(name)) {
|
|
3775
|
+
if (path.length !== 1)
|
|
3776
|
+
return [];
|
|
3777
|
+
return name
|
|
3778
|
+
.getElements()
|
|
3779
|
+
.filter((element) => !element.getDotDotDotToken() &&
|
|
3780
|
+
(element.getPropertyNameNode()?.getText() ?? element.getName()) === path[0] &&
|
|
3781
|
+
Node.isIdentifier(element.getNameNode()))
|
|
3782
|
+
.map((element) => element.getNameNode());
|
|
3783
|
+
}
|
|
3784
|
+
if (!Node.isIdentifier(name))
|
|
3785
|
+
return [];
|
|
3786
|
+
const symbol = name.getSymbol()?.compilerSymbol;
|
|
3787
|
+
if (!symbol)
|
|
3788
|
+
return [];
|
|
3789
|
+
return func
|
|
3790
|
+
.getDescendantsOfKind(SyntaxKind.Identifier)
|
|
3791
|
+
.filter((id) => id !== name && id.getSymbol()?.compilerSymbol === symbol)
|
|
3792
|
+
.map(follow)
|
|
3793
|
+
.filter((read) => read !== undefined);
|
|
3794
|
+
}
|
|
3463
3795
|
/**
|
|
3464
3796
|
* The decided abstain of a call whose result carries transport the
|
|
3465
3797
|
* service's wrapper rules verify and read no payload out of (carrick#1841,
|
|
@@ -7630,6 +7962,15 @@ export class TypeInferrer {
|
|
|
7630
7962
|
* carrying the provenance the reading recorded.
|
|
7631
7963
|
*/
|
|
7632
7964
|
declaredRequestInferredType(request, contract, location) {
|
|
7965
|
+
if (contract.noRequestBody !== undefined) {
|
|
7966
|
+
this.log(`Route at ${request.file_path}:${request.line_number} states no request body: ${contract.noRequestBody}`);
|
|
7967
|
+
return this.decidedAbstain(request, location, {
|
|
7968
|
+
path: '',
|
|
7969
|
+
kind: 'unknown',
|
|
7970
|
+
reason: 'no_request_body',
|
|
7971
|
+
detail: contract.noRequestBody,
|
|
7972
|
+
});
|
|
7973
|
+
}
|
|
7633
7974
|
const inferred = this.createInferredType(request, contract.text, true, location, undefined, undefined);
|
|
7634
7975
|
if (contract.provenance && contract.provenance.length > 0) {
|
|
7635
7976
|
inferred.any_provenance = contract.provenance;
|
|
@@ -7648,12 +7989,74 @@ export class TypeInferrer {
|
|
|
7648
7989
|
* own locator authoritative when a route declares nothing.
|
|
7649
7990
|
*/
|
|
7650
7991
|
declaredRequestContract(registration, handler) {
|
|
7992
|
+
// carrick#2674: the body member a verified label names, read at the
|
|
7993
|
+
// handler's parameter, comes first. Where no label answers, the old rule
|
|
7994
|
+
// that reads a member by its name stands.
|
|
7995
|
+
const stated = this.statedParams.request(handler, registration);
|
|
7996
|
+
if (stated?.kind === 'body') {
|
|
7997
|
+
const text = this.structuralTextFromType(stated.type, stated.at);
|
|
7998
|
+
if (text) {
|
|
7999
|
+
return { text };
|
|
8000
|
+
}
|
|
8001
|
+
}
|
|
7651
8002
|
const annotated = this.requestBodyFromHandlerParams(handler);
|
|
7652
8003
|
if (annotated) {
|
|
7653
8004
|
return { text: annotated };
|
|
7654
8005
|
}
|
|
7655
|
-
|
|
7656
|
-
this.validatedBodyContract(registration)
|
|
8006
|
+
const schema = this.routeSchemaContract(registration, 'body') ??
|
|
8007
|
+
this.validatedBodyContract(registration);
|
|
8008
|
+
if (schema) {
|
|
8009
|
+
return schema;
|
|
8010
|
+
}
|
|
8011
|
+
return stated?.kind === 'no_body' ? { text: 'unknown', noRequestBody: stated.detail } : null;
|
|
8012
|
+
}
|
|
8013
|
+
/** The stated-parameter reader over `labels`, judging files and statuses as this inferrer does. */
|
|
8014
|
+
statedParamsFor(labels) {
|
|
8015
|
+
return new StatedParams(labels, (file) => {
|
|
8016
|
+
const sourceFile = file.getSourceFile();
|
|
8017
|
+
if (sourceFile.isDeclarationFile())
|
|
8018
|
+
return false;
|
|
8019
|
+
return !isExternalOrigin(this.project.getProgram().compilerObject, sourceFile.compilerNode, this.repoRoot, externalImportsOf(this.project));
|
|
8020
|
+
}, (call) => this.chainStatus(call));
|
|
8021
|
+
}
|
|
8022
|
+
/**
|
|
8023
|
+
* carrick#2674: where today's reading of a route's response is empty
|
|
8024
|
+
* (nothing, `void`, or an `unknown` the inferrer did not decide), the reply
|
|
8025
|
+
* its handler's parameter states, cut to its success variants. A typed
|
|
8026
|
+
* reading, or a decided abstain, stands as it is.
|
|
8027
|
+
*/
|
|
8028
|
+
withStatedReply(sourceFile, request, today) {
|
|
8029
|
+
if (!this.statedParams.readsReplies)
|
|
8030
|
+
return today;
|
|
8031
|
+
if (today && !this.isEmptyResponseReading(today))
|
|
8032
|
+
return today;
|
|
8033
|
+
const handler = this.registrationAtLine(sourceFile, request.line_number)?.handler ??
|
|
8034
|
+
this.exportedFunctionOpeningOn(sourceFile, request.line_number);
|
|
8035
|
+
if (!handler)
|
|
8036
|
+
return today;
|
|
8037
|
+
const stated = this.statedParams.reply(handler);
|
|
8038
|
+
if (!stated)
|
|
8039
|
+
return today;
|
|
8040
|
+
const texts = [];
|
|
8041
|
+
for (const variant of stated.variants) {
|
|
8042
|
+
const text = this.structuralTextFromType(variant, stated.at);
|
|
8043
|
+
// A variant holding `any` or `unknown` is one the check refuses as an
|
|
8044
|
+
// anchor (`usable_inferred_text`): publishing it would fall back to the
|
|
8045
|
+
// raw anchor under an explicit flag, so today's reading stands.
|
|
8046
|
+
if (!text || printedTypeHoldsTopType(text))
|
|
8047
|
+
return today;
|
|
8048
|
+
texts.push(text);
|
|
8049
|
+
}
|
|
8050
|
+
this.log(`Route at ${request.file_path}:${request.line_number}: its handler's reply parameter states the body`);
|
|
8051
|
+
return this.createInferredType(request, texts.length === 1 ? texts[0] : texts.map((text) => `(${text})`).join(' | '), true, this.getNodeLocation(handler));
|
|
8052
|
+
}
|
|
8053
|
+
/** Nothing read: `void`, or an `unknown` with no decided reason at its root. */
|
|
8054
|
+
isEmptyResponseReading(inferred) {
|
|
8055
|
+
if (inferred.type_string === 'void')
|
|
8056
|
+
return true;
|
|
8057
|
+
if (inferred.type_string !== 'unknown')
|
|
8058
|
+
return false;
|
|
8059
|
+
return !(inferred.any_provenance ?? []).some((provenance) => provenance.path === '' && DECIDED_ABSTAIN_REASONS.has(provenance.reason));
|
|
7657
8060
|
}
|
|
7658
8061
|
/**
|
|
7659
8062
|
* Anchor (a): the request contract declared on the handler's own signature.
|
|
@@ -7866,7 +8269,7 @@ export class TypeInferrer {
|
|
|
7866
8269
|
return;
|
|
7867
8270
|
const text = this.structuralTextFromTypeNode(typeNode);
|
|
7868
8271
|
if (text)
|
|
7869
|
-
found.push({ rank, text, at, how });
|
|
8272
|
+
found.push({ rank, text, at, how, typeNode });
|
|
7870
8273
|
};
|
|
7871
8274
|
for (let i = 0; i < positions.length; i++) {
|
|
7872
8275
|
const position = positions[i];
|
|
@@ -7953,7 +8356,7 @@ export class TypeInferrer {
|
|
|
7953
8356
|
return null;
|
|
7954
8357
|
}
|
|
7955
8358
|
const [first] = decided;
|
|
7956
|
-
return { text: first.text, provenance: first.provenance, at: first.at, how: first.how };
|
|
8359
|
+
return { text: first.text, provenance: first.provenance, at: first.at, how: first.how, typeNode: first.typeNode };
|
|
7957
8360
|
}
|
|
7958
8361
|
/**
|
|
7959
8362
|
* What a call that takes the value at argument `index` states for it:
|
|
@@ -9178,3 +9581,12 @@ export class TypeInferrer {
|
|
|
9178
9581
|
console.error(`[sidecar:type-inferrer:error] ${message}`);
|
|
9179
9582
|
}
|
|
9180
9583
|
}
|
|
9584
|
+
/** The node a `SavedSpan` was taken from, in the file as it reads now. */
|
|
9585
|
+
function nodeAtSpan(span) {
|
|
9586
|
+
let node = span.file.getDescendantAtPos(span.start);
|
|
9587
|
+
while (node &&
|
|
9588
|
+
!(node.getStart() === span.start && node.getEnd() === span.end && node.getKind() === span.kind)) {
|
|
9589
|
+
node = node.getParent();
|
|
9590
|
+
}
|
|
9591
|
+
return node;
|
|
9592
|
+
}
|