carrick 0.3.126 → 0.3.128
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 +56 -4
- package/sidecar/dist/src/library-claims.d.ts +63 -3
- package/sidecar/dist/src/library-claims.js +435 -44
- package/sidecar/dist/src/retype.js +124 -8
- 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 +144 -8
- package/sidecar/dist/src/type-inferrer.js +635 -35
- package/sidecar/dist/src/types.d.ts +84 -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';
|
|
@@ -176,11 +177,29 @@ const RESPONSE_INIT_MEMBER_NAMES = new Set([
|
|
|
176
177
|
* two; the bound is a backstop against a pathologically nested return.
|
|
177
178
|
*/
|
|
178
179
|
const RESPONSE_HELPER_MAX_DEPTH = 4;
|
|
180
|
+
/**
|
|
181
|
+
* How many wrapper calls a handler bound to one is followed through
|
|
182
|
+
* (carrick#2472 P3-2b): `wrap(trace(async (req, res) => …))` needs two. A
|
|
183
|
+
* backstop against a pathologically nested initializer.
|
|
184
|
+
*/
|
|
185
|
+
const WRAPPER_CALL_MAX_DEPTH = 3;
|
|
179
186
|
/**
|
|
180
187
|
* Members an options object uses to state an HTTP status. A >= 400 status marks
|
|
181
188
|
* the branch an error path, whose shape is not the endpoint's contract.
|
|
182
189
|
*/
|
|
183
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
|
+
]);
|
|
184
203
|
/** The provenance sentence for each reason a branch was not read. */
|
|
185
204
|
const SERIALISED_BODY_UNREAD_DETAIL = {
|
|
186
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.',
|
|
@@ -442,13 +461,16 @@ export class TypeInferrer {
|
|
|
442
461
|
* @param onRequestDone - Called once per request, as the batch is done with it
|
|
443
462
|
* @returns InferResult with inferred types or errors
|
|
444
463
|
*/
|
|
445
|
-
infer(requests, extractionConfig, onRequestDone, modelSymbols) {
|
|
464
|
+
infer(requests, extractionConfig, onRequestDone, modelSymbols, labels) {
|
|
446
465
|
const started = performance.now();
|
|
447
466
|
this.modelSymbols = new Map((modelSymbols ?? []).map((symbol) => [symbol.alias, symbol]));
|
|
467
|
+
this.statedParams = this.statedParamsFor(labels ?? { sends: new Map(), bodies: new Set() });
|
|
448
468
|
const inferredTypes = [];
|
|
449
469
|
const errors = [];
|
|
450
470
|
/** Response inferences the unwidened reading re-reads (carrick#1516). */
|
|
451
471
|
const responses = [];
|
|
472
|
+
/** Request-body inferences it re-reads after them (carrick#2011). */
|
|
473
|
+
const bodies = [];
|
|
452
474
|
/** How long each request took, in the order they were done (carrick#1985). */
|
|
453
475
|
const timings = [];
|
|
454
476
|
for (const request of requests) {
|
|
@@ -485,6 +507,9 @@ export class TypeInferrer {
|
|
|
485
507
|
if (request.infer_kind === 'response_body' || request.infer_kind === 'function_return') {
|
|
486
508
|
responses.push({ request, result });
|
|
487
509
|
}
|
|
510
|
+
else if (request.infer_kind === 'request_body') {
|
|
511
|
+
bodies.push({ request, result });
|
|
512
|
+
}
|
|
488
513
|
}
|
|
489
514
|
else {
|
|
490
515
|
errors.push(`Could not infer type at ${request.file_path}:${loc} (${request.infer_kind})`);
|
|
@@ -515,10 +540,47 @@ export class TypeInferrer {
|
|
|
515
540
|
}
|
|
516
541
|
try {
|
|
517
542
|
const deadline = Math.min(performance.now() + this.unwidenedBudgetMs, started + UNWIDENED_LATEST_MS);
|
|
518
|
-
|
|
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
|
+
}
|
|
519
582
|
}
|
|
520
583
|
catch (err) {
|
|
521
|
-
// The reading only ever adds a field; it never costs an answer.
|
|
522
584
|
this.logError(`Unwidened reading failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
523
585
|
}
|
|
524
586
|
return {
|
|
@@ -530,6 +592,12 @@ export class TypeInferrer {
|
|
|
530
592
|
}
|
|
531
593
|
/** The types the model named, by the alias of the row each is for (carrick#2111). */
|
|
532
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() });
|
|
533
601
|
/**
|
|
534
602
|
* carrick#2111: where the model named a type for the response this request
|
|
535
603
|
* reads, record on `inferred` how that type relates to the compiler's type
|
|
@@ -614,19 +682,29 @@ export class TypeInferrer {
|
|
|
614
682
|
* really returns beside the published one (`unwidened_type_string`). See
|
|
615
683
|
* `unwidened.ts` for what is marked and why the reading is sound.
|
|
616
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
|
+
*
|
|
617
689
|
* One rewrite for the whole batch: every file on any request's path is
|
|
618
690
|
* rewritten once, type-checked before and after, the requests re-inferred,
|
|
619
691
|
* and every file restored before returning, so the project other requests
|
|
620
692
|
* read is the project the scan loaded.
|
|
621
693
|
*
|
|
622
694
|
* Nothing is read past `deadline`: a batch that runs out keeps the readings
|
|
623
|
-
* it made and publishes none for the rest
|
|
695
|
+
* it made and publishes none for the rest, and marks each of the rest
|
|
696
|
+
* `unwidened_out_of_budget` (carrick#2100). A request that never needed a
|
|
697
|
+
* reading (a declared return, nothing to mark) or lost it for a reason of
|
|
698
|
+
* its own (a marking that added a diagnostic) is never marked.
|
|
624
699
|
*/
|
|
625
|
-
addUnwidenedReadings(responses, extractionConfig, deadline) {
|
|
626
|
-
const outOfTime = (stage) => {
|
|
700
|
+
addUnwidenedReadings(responses, extractionConfig, deadline, read) {
|
|
701
|
+
const outOfTime = (stage, unread) => {
|
|
627
702
|
if (performance.now() <= deadline)
|
|
628
703
|
return false;
|
|
629
|
-
this.log(`Unwidened reading stopped ${stage}: it ran out of its budget`
|
|
704
|
+
this.log(`Unwidened reading stopped ${stage}: it ran out of its budget, ` +
|
|
705
|
+
`${unread.length} reading(s) not made`);
|
|
706
|
+
for (const plan of unread)
|
|
707
|
+
plan.result.unwidened_out_of_budget = true;
|
|
630
708
|
return true;
|
|
631
709
|
};
|
|
632
710
|
const plans = [];
|
|
@@ -634,17 +712,19 @@ export class TypeInferrer {
|
|
|
634
712
|
const checkedFiles = new Set();
|
|
635
713
|
// Every position is read before the first rewrite: a rewrite forgets
|
|
636
714
|
// every node of its file.
|
|
637
|
-
for (const { request, result } of responses) {
|
|
715
|
+
for (const { request, result, readSpan } of responses) {
|
|
638
716
|
// A declared return type is the contract; nothing was widened.
|
|
639
717
|
if (result.is_explicit)
|
|
640
718
|
continue;
|
|
641
|
-
const
|
|
642
|
-
|
|
719
|
+
const readNode = readSpan
|
|
720
|
+
? nodeAtSpan(readSpan)
|
|
721
|
+
: this.readNodes.get(result.source_location);
|
|
722
|
+
if (!readNode)
|
|
643
723
|
continue;
|
|
644
|
-
const functions = pathOf(
|
|
724
|
+
const functions = pathOf(readNode);
|
|
645
725
|
let marked = 0;
|
|
646
726
|
for (const fn of functions) {
|
|
647
|
-
const insertions = literalInsertions(fn);
|
|
727
|
+
const insertions = literalInsertions(fn, read === 'request_body' ? readNode : undefined);
|
|
648
728
|
marked += insertions.length;
|
|
649
729
|
if (insertions.length === 0)
|
|
650
730
|
continue;
|
|
@@ -673,7 +753,7 @@ export class TypeInferrer {
|
|
|
673
753
|
plans.push({
|
|
674
754
|
request,
|
|
675
755
|
result,
|
|
676
|
-
read: { file:
|
|
756
|
+
read: { file: readNode.getSourceFile(), start: readNode.getStart(), end: readNode.getEnd() },
|
|
677
757
|
path,
|
|
678
758
|
locatedByText,
|
|
679
759
|
});
|
|
@@ -685,7 +765,7 @@ export class TypeInferrer {
|
|
|
685
765
|
insertions.set(file, normalise(list));
|
|
686
766
|
const before = new Map();
|
|
687
767
|
for (const file of checkedFiles) {
|
|
688
|
-
if (outOfTime('before the rewrite'))
|
|
768
|
+
if (outOfTime('before the rewrite', plans))
|
|
689
769
|
return;
|
|
690
770
|
before.set(file, fileDiagnostics(file));
|
|
691
771
|
}
|
|
@@ -701,7 +781,7 @@ export class TypeInferrer {
|
|
|
701
781
|
// holds it loses its reading.
|
|
702
782
|
const tainted = new Set();
|
|
703
783
|
for (const file of checkedFiles) {
|
|
704
|
-
if (outOfTime('while checking the rewrite'))
|
|
784
|
+
if (outOfTime('while checking the rewrite', plans))
|
|
705
785
|
return;
|
|
706
786
|
const list = insertions.get(file) ?? [];
|
|
707
787
|
for (const at of addedDiagnostics(before.get(file), fileDiagnostics(file), list)) {
|
|
@@ -713,9 +793,10 @@ export class TypeInferrer {
|
|
|
713
793
|
}
|
|
714
794
|
}
|
|
715
795
|
}
|
|
716
|
-
for (const plan of plans) {
|
|
717
|
-
if (outOfTime('while re-reading'))
|
|
796
|
+
for (const [at, plan] of plans.entries()) {
|
|
797
|
+
if (outOfTime('while re-reading', plans.slice(at).filter((rest) => !tainted.has(rest)))) {
|
|
718
798
|
return;
|
|
799
|
+
}
|
|
719
800
|
if (tainted.has(plan)) {
|
|
720
801
|
this.log(`Unwidened reading for ${plan.request.file_path}:${plan.request.line_number} ` +
|
|
721
802
|
'dropped: marking its literals added a diagnostic on its path');
|
|
@@ -794,7 +875,7 @@ export class TypeInferrer {
|
|
|
794
875
|
case 'function_return':
|
|
795
876
|
return this.inferFunctionReturn(sourceFile, request, extractionConfig);
|
|
796
877
|
case 'response_body':
|
|
797
|
-
return this.inferResponseBody(sourceFile, request, extractionConfig);
|
|
878
|
+
return this.withStatedReply(sourceFile, request, this.inferResponseBody(sourceFile, request, extractionConfig));
|
|
798
879
|
case 'call_result':
|
|
799
880
|
return this.inferCallResult(sourceFile, request, extractionConfig);
|
|
800
881
|
case 'variable':
|
|
@@ -901,6 +982,16 @@ export class TypeInferrer {
|
|
|
901
982
|
if (this.serialisedBodyEnvelopeEngages(awaitedType, func)) {
|
|
902
983
|
return this.inferSerialisedBodyEnvelope(request, func);
|
|
903
984
|
}
|
|
985
|
+
// A route handler that returns the reply it was handed (`return reply`)
|
|
986
|
+
// states no body by that return; the reply's type is built from the
|
|
987
|
+
// route's request type argument (carrick-cloud#2576).
|
|
988
|
+
const returned = this.returnedExpressions(func);
|
|
989
|
+
if (returned.length > 0 &&
|
|
990
|
+
returned.every((expression) => this.isRouteHandlerParameter(this.unwrapExpressionNode(expression), request))) {
|
|
991
|
+
this.log(`Return at ${request.file_path}:${request.line_number} is an object the ` +
|
|
992
|
+
'framework handed the handler; it states no body');
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
904
995
|
const unwrapResult = this.unwrapTypeWithConfig(awaitedType, func, extractionConfig);
|
|
905
996
|
if (unwrapResult.wasUnwrapped && !unwrapResult.verifiedMachinery) {
|
|
906
997
|
typeString = unwrapResult.typeString;
|
|
@@ -1278,6 +1369,16 @@ export class TypeInferrer {
|
|
|
1278
1369
|
payloadNode = args[0];
|
|
1279
1370
|
}
|
|
1280
1371
|
}
|
|
1372
|
+
// A parameter the route registration hands its handler is never the
|
|
1373
|
+
// route's body: not the reply a handler returns (`return reply`), and not
|
|
1374
|
+
// the request the drill reaches in a call that forwards it
|
|
1375
|
+
// (`return finish(request, reply)`), whose type the framework built from
|
|
1376
|
+
// the route's request type argument (carrick-cloud#2576).
|
|
1377
|
+
if (this.isRouteHandlerParameter(payloadNode, request)) {
|
|
1378
|
+
this.log(`Response payload at ${request.file_path}:${request.line_number} is an object ` +
|
|
1379
|
+
'the framework handed the handler; it states no body');
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1281
1382
|
const payloadType = payloadNode.getType();
|
|
1282
1383
|
let typeString = typeText(payloadType, payloadNode);
|
|
1283
1384
|
const unwrapResult = this.unwrapTypeWithConfig(payloadType, payloadNode, extractionConfig);
|
|
@@ -1447,6 +1548,30 @@ export class TypeInferrer {
|
|
|
1447
1548
|
}
|
|
1448
1549
|
return this.receivingCallOf(located, isReturned);
|
|
1449
1550
|
}
|
|
1551
|
+
/**
|
|
1552
|
+
* True when `node` is a bare reference to a parameter of the handler the
|
|
1553
|
+
* route registration on the request's line hands in (found as
|
|
1554
|
+
* `registrationAtLine` finds it, wrappers included): the request, the reply
|
|
1555
|
+
* or the context the framework passes, never a body the route sends
|
|
1556
|
+
* (carrick-cloud#2576). Where the parameter's type is declared is not read:
|
|
1557
|
+
* a parameter of any other function is data, whether the repo or an
|
|
1558
|
+
* installed package types it (`.then((account) => reply.send(account))`, a
|
|
1559
|
+
* helper's `account: Account`).
|
|
1560
|
+
*/
|
|
1561
|
+
isRouteHandlerParameter(node, request) {
|
|
1562
|
+
if (!Node.isIdentifier(node))
|
|
1563
|
+
return false;
|
|
1564
|
+
const owners = (node.getSymbol()?.getDeclarations() ?? [])
|
|
1565
|
+
.filter((decl) => Node.isParameterDeclaration(decl))
|
|
1566
|
+
.map((decl) => decl.getParent().compilerNode);
|
|
1567
|
+
if (owners.length === 0)
|
|
1568
|
+
return false;
|
|
1569
|
+
const sourceFile = this.project.getSourceFile(request.file_path);
|
|
1570
|
+
if (!sourceFile)
|
|
1571
|
+
return false;
|
|
1572
|
+
const handler = this.registrationAtLine(sourceFile, request.line_number)?.handler;
|
|
1573
|
+
return handler !== undefined && owners.includes(handler.compilerNode);
|
|
1574
|
+
}
|
|
1450
1575
|
/**
|
|
1451
1576
|
* True when a located call's own result is the route's payload, so the
|
|
1452
1577
|
* transitional drill into its first argument must not run (carrick#1732).
|
|
@@ -1852,6 +1977,16 @@ export class TypeInferrer {
|
|
|
1852
1977
|
typeString = explicitType;
|
|
1853
1978
|
isExplicit = true;
|
|
1854
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
|
+
}
|
|
1855
1990
|
// carrick#1843: a rule can now match a thenable or a carrier by its alias,
|
|
1856
1991
|
// so on a result that is a carrier (`Task<Outcome<Reply<T>, E>>`) the
|
|
1857
1992
|
// rules reach what the carrier holds before the carrier read below does.
|
|
@@ -1863,6 +1998,17 @@ export class TypeInferrer {
|
|
|
1863
1998
|
const callArgs = this.callResultArguments(callExpr);
|
|
1864
1999
|
const resultIsCarrier = !explicitType &&
|
|
1865
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
|
+
}
|
|
1866
2012
|
// carrick#1877: the terminal holds a thenable the names do not peel, a
|
|
1867
2013
|
// subclass of `Promise` or a class with a `then` of its own. What a
|
|
1868
2014
|
// caller receives is what `await` yields for it, as it is for the
|
|
@@ -2076,7 +2222,9 @@ export class TypeInferrer {
|
|
|
2076
2222
|
const leftCarrier = (callUnwrap.wasUnwrapped || readByProtocol) &&
|
|
2077
2223
|
!!callAfterRules &&
|
|
2078
2224
|
this.resultCarrierArguments(callAfterRules, callExpr, callArgs) !== undefined;
|
|
2079
|
-
const anchorSource =
|
|
2225
|
+
const anchorSource = narrowed?.typeNode
|
|
2226
|
+
? narrowed.typeNode.getType()
|
|
2227
|
+
: carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
|
|
2080
2228
|
let anchor = anchorSource
|
|
2081
2229
|
? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
|
|
2082
2230
|
: undefined;
|
|
@@ -2125,7 +2273,11 @@ export class TypeInferrer {
|
|
|
2125
2273
|
// carrick#1749: say when the text is the source's own statement of the
|
|
2126
2274
|
// body, and what that statement is rooted at, so the scanner can tell a
|
|
2127
2275
|
// model symbol that names the body from one that names a part of it.
|
|
2128
|
-
const statedBody =
|
|
2276
|
+
const statedBody = narrowed?.typeNode
|
|
2277
|
+
? this.statedRoot(narrowed.typeNode)
|
|
2278
|
+
: explicitType
|
|
2279
|
+
? this.statedBodyAtRead(terminalNode)
|
|
2280
|
+
: undefined;
|
|
2129
2281
|
if (statedBody) {
|
|
2130
2282
|
inferred.stated_body = statedBody;
|
|
2131
2283
|
}
|
|
@@ -3403,6 +3555,243 @@ export class TypeInferrer {
|
|
|
3403
3555
|
}
|
|
3404
3556
|
return common;
|
|
3405
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
|
+
}
|
|
3406
3795
|
/**
|
|
3407
3796
|
* The decided abstain of a call whose result carries transport the
|
|
3408
3797
|
* service's wrapper rules verify and read no payload out of (carrick#1841,
|
|
@@ -6305,14 +6694,17 @@ export class TypeInferrer {
|
|
|
6305
6694
|
return undefined;
|
|
6306
6695
|
}
|
|
6307
6696
|
/**
|
|
6308
|
-
* The handler function referenced by a route-registration call: the
|
|
6697
|
+
* The handler function referenced by a route-registration call: the LAST
|
|
6309
6698
|
* argument that is an inline function, or an identifier bound to a function
|
|
6310
|
-
* (
|
|
6311
|
-
*
|
|
6312
|
-
*
|
|
6699
|
+
* (carrick#2472 P3-2b). The functions before it are middleware
|
|
6700
|
+
* (`route(path).post(auth, audit, handler)`), which runs first and hands on;
|
|
6701
|
+
* reading the first function read a middleware as the route. The path
|
|
6702
|
+
* literal is not function-valued, so it is skipped naturally. No callee-name
|
|
6703
|
+
* check: a call whose argument resolves to a function is structurally a
|
|
6704
|
+
* registration regardless of the framework.
|
|
6313
6705
|
*/
|
|
6314
6706
|
handlerFromRegistrationCall(call) {
|
|
6315
|
-
for (const arg of call.getArguments()) {
|
|
6707
|
+
for (const arg of [...call.getArguments()].reverse()) {
|
|
6316
6708
|
const handler = this.asHandlerFunction(arg);
|
|
6317
6709
|
if (handler) {
|
|
6318
6710
|
return handler;
|
|
@@ -6351,15 +6743,17 @@ export class TypeInferrer {
|
|
|
6351
6743
|
* declaration (via the compiler's definition nodes) and returned when that
|
|
6352
6744
|
* declaration is — or initializes to — a function. Anything else (a path
|
|
6353
6745
|
* literal, an object, a non-function binding) yields undefined.
|
|
6746
|
+
* `followWrapperCalls` false reads a binding to a wrapper call as no
|
|
6747
|
+
* function, so a name the wrapper walk meets is never walked again.
|
|
6354
6748
|
*/
|
|
6355
|
-
asHandlerFunction(node) {
|
|
6749
|
+
asHandlerFunction(node, followWrapperCalls = true) {
|
|
6356
6750
|
const expr = this.unwrapExpressionNode(node);
|
|
6357
6751
|
if (Node.isArrowFunction(expr) || Node.isFunctionExpression(expr)) {
|
|
6358
6752
|
return expr;
|
|
6359
6753
|
}
|
|
6360
6754
|
if (Node.isIdentifier(expr)) {
|
|
6361
6755
|
for (const def of expr.getDefinitionNodes()) {
|
|
6362
|
-
const func = this.functionFromDeclaration(def);
|
|
6756
|
+
const func = this.functionFromDeclaration(def, followWrapperCalls);
|
|
6363
6757
|
if (func) {
|
|
6364
6758
|
return func;
|
|
6365
6759
|
}
|
|
@@ -6371,10 +6765,13 @@ export class TypeInferrer {
|
|
|
6371
6765
|
* Extract a `FunctionLike` from a declaration node the compiler resolved an
|
|
6372
6766
|
* identifier to: a function declaration is itself the function; a variable
|
|
6373
6767
|
* declaration / binding whose initializer is an inline function yields that
|
|
6374
|
-
* function (`const h = async () => { … }`)
|
|
6375
|
-
*
|
|
6768
|
+
* function (`const h = async () => { … }`), and one whose initializer is a
|
|
6769
|
+
* wrapper call yields the function the call wraps
|
|
6770
|
+
* (`const h = wrap<Contract>(async (req, res) => { … })`, carrick#2472
|
|
6771
|
+
* P3-2b; `functionWrappedBy`). Import/re-export shims are walked by
|
|
6772
|
+
* ts-morph's `getDefinitionNodes`, so no manual import chasing is needed.
|
|
6376
6773
|
*/
|
|
6377
|
-
functionFromDeclaration(decl) {
|
|
6774
|
+
functionFromDeclaration(decl, followWrapperCalls = true) {
|
|
6378
6775
|
if (Node.isFunctionDeclaration(decl)) {
|
|
6379
6776
|
return decl;
|
|
6380
6777
|
}
|
|
@@ -6399,10 +6796,43 @@ export class TypeInferrer {
|
|
|
6399
6796
|
if (Node.isArrowFunction(inner) || Node.isFunctionExpression(inner)) {
|
|
6400
6797
|
return inner;
|
|
6401
6798
|
}
|
|
6799
|
+
if (followWrapperCalls && Node.isCallExpression(inner)) {
|
|
6800
|
+
return this.functionWrappedBy(inner, 0);
|
|
6801
|
+
}
|
|
6402
6802
|
}
|
|
6403
6803
|
}
|
|
6404
6804
|
return undefined;
|
|
6405
6805
|
}
|
|
6806
|
+
/**
|
|
6807
|
+
* The function a call wraps, for a name bound to that call and used as a
|
|
6808
|
+
* handler (carrick#2472 P3-2b): the call's one argument that is a function
|
|
6809
|
+
* literal, a name bound to one, or another such call, a few calls deep.
|
|
6810
|
+
*
|
|
6811
|
+
* Two conditions keep this to wrappers. The call's own value has to be
|
|
6812
|
+
* callable, because the name is used as a function: `rows.map((row) => …)`
|
|
6813
|
+
* is handed a function and yields a list. And exactly one argument may be a
|
|
6814
|
+
* function: with two there is no saying which of them is wrapped.
|
|
6815
|
+
*/
|
|
6816
|
+
functionWrappedBy(call, depth) {
|
|
6817
|
+
if (depth >= WRAPPER_CALL_MAX_DEPTH)
|
|
6818
|
+
return undefined;
|
|
6819
|
+
if (!this.isCallableType(call.getType()))
|
|
6820
|
+
return undefined;
|
|
6821
|
+
const wrapped = [];
|
|
6822
|
+
for (const arg of call.getArguments()) {
|
|
6823
|
+
const value = this.unwrapExpressionNode(arg);
|
|
6824
|
+
const func = Node.isArrowFunction(value) || Node.isFunctionExpression(value)
|
|
6825
|
+
? value
|
|
6826
|
+
: Node.isCallExpression(value)
|
|
6827
|
+
? this.functionWrappedBy(value, depth + 1)
|
|
6828
|
+
: Node.isIdentifier(value)
|
|
6829
|
+
? this.asHandlerFunction(value, false)
|
|
6830
|
+
: undefined;
|
|
6831
|
+
if (func)
|
|
6832
|
+
wrapped.push(func);
|
|
6833
|
+
}
|
|
6834
|
+
return wrapped.length === 1 ? wrapped[0] : undefined;
|
|
6835
|
+
}
|
|
6406
6836
|
/**
|
|
6407
6837
|
* True when `handler` reads nothing from the incoming message (carrick#827):
|
|
6408
6838
|
* every use of one of its parameters, named or destructured, is the receiver
|
|
@@ -7045,6 +7475,15 @@ export class TypeInferrer {
|
|
|
7045
7475
|
* below reads it.
|
|
7046
7476
|
*/
|
|
7047
7477
|
bodyBuiltByCallee(node) {
|
|
7478
|
+
const read = this.calleeBodyRead(node, 0);
|
|
7479
|
+
return read.kind === 'passes' ? { kind: 'none' } : read;
|
|
7480
|
+
}
|
|
7481
|
+
/**
|
|
7482
|
+
* `bodyBuiltByCallee`, telling apart a callee that sends the argument
|
|
7483
|
+
* unchanged (`passes`) from one that states nothing about it (`none`).
|
|
7484
|
+
* `depth` counts the forwarded locals followed (carrick#2671).
|
|
7485
|
+
*/
|
|
7486
|
+
calleeBodyRead(node, depth) {
|
|
7048
7487
|
const own = node.getType();
|
|
7049
7488
|
if (own.isAny() || own.isUnknown())
|
|
7050
7489
|
return { kind: 'none' };
|
|
@@ -7107,6 +7546,15 @@ export class TypeInferrer {
|
|
|
7107
7546
|
built.push(value);
|
|
7108
7547
|
continue;
|
|
7109
7548
|
}
|
|
7549
|
+
// carrick#2671: the use builds a local (`const payload = { ...input,
|
|
7550
|
+
// code: norm(input.code) }`) the function then sends unchanged, itself
|
|
7551
|
+
// or through a helper that passes it on as the body. The body is that
|
|
7552
|
+
// local.
|
|
7553
|
+
const forwarded = this.forwardedLocalHolding(use, body, depth);
|
|
7554
|
+
if (forwarded) {
|
|
7555
|
+
built.push(forwarded);
|
|
7556
|
+
continue;
|
|
7557
|
+
}
|
|
7110
7558
|
const ancestors = use.getAncestors();
|
|
7111
7559
|
const insideLiteral = ancestors
|
|
7112
7560
|
.slice(0, ancestors.indexOf(body))
|
|
@@ -7119,7 +7567,7 @@ export class TypeInferrer {
|
|
|
7119
7567
|
}
|
|
7120
7568
|
}
|
|
7121
7569
|
if (built.length === 0)
|
|
7122
|
-
return { kind: 'none' };
|
|
7570
|
+
return passedThrough ? { kind: 'passes' } : { kind: 'none' };
|
|
7123
7571
|
if (passedThrough) {
|
|
7124
7572
|
return { kind: 'abstain', why: `the called function both sends the argument and builds a body from it (${from})` };
|
|
7125
7573
|
}
|
|
@@ -7170,6 +7618,78 @@ export class TypeInferrer {
|
|
|
7170
7618
|
}
|
|
7171
7619
|
return undefined;
|
|
7172
7620
|
}
|
|
7621
|
+
/**
|
|
7622
|
+
* carrick#2671: the `const` local whose initializer holds `use`, inside
|
|
7623
|
+
* `scope`, when `scope` sends that local unchanged as the body: whole in a
|
|
7624
|
+
* body slot, or as an argument into a function that passes it on as its
|
|
7625
|
+
* body. The local's name, so its declared type is read. Undefined when the
|
|
7626
|
+
* local is never sent, when a further body is built from it, or when a
|
|
7627
|
+
* function lies between `use` and the declaration.
|
|
7628
|
+
*/
|
|
7629
|
+
forwardedLocalHolding(use, scope, depth) {
|
|
7630
|
+
if (depth >= 2)
|
|
7631
|
+
return undefined;
|
|
7632
|
+
let current = use;
|
|
7633
|
+
let declaration;
|
|
7634
|
+
while (current !== scope) {
|
|
7635
|
+
const parent = current.getParent();
|
|
7636
|
+
if (!parent)
|
|
7637
|
+
return undefined;
|
|
7638
|
+
if (Node.isArrowFunction(parent) ||
|
|
7639
|
+
Node.isFunctionExpression(parent) ||
|
|
7640
|
+
Node.isFunctionDeclaration(parent) ||
|
|
7641
|
+
Node.isMethodDeclaration(parent)) {
|
|
7642
|
+
return undefined;
|
|
7643
|
+
}
|
|
7644
|
+
if (Node.isVariableDeclaration(parent) && parent.getInitializer() === current) {
|
|
7645
|
+
declaration = parent;
|
|
7646
|
+
break;
|
|
7647
|
+
}
|
|
7648
|
+
current = parent;
|
|
7649
|
+
}
|
|
7650
|
+
if (!declaration)
|
|
7651
|
+
return undefined;
|
|
7652
|
+
if (declaration.getVariableStatement()?.getDeclarationKind() !== VariableDeclarationKind.Const) {
|
|
7653
|
+
return undefined;
|
|
7654
|
+
}
|
|
7655
|
+
const name = declaration.getNameNode();
|
|
7656
|
+
if (!Node.isIdentifier(name))
|
|
7657
|
+
return undefined;
|
|
7658
|
+
const symbol = name.getSymbol()?.compilerSymbol;
|
|
7659
|
+
if (!symbol)
|
|
7660
|
+
return undefined;
|
|
7661
|
+
const checker = this.project.getTypeChecker().compilerObject;
|
|
7662
|
+
let sent = false;
|
|
7663
|
+
for (const ref of scope.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
7664
|
+
if (ref === name)
|
|
7665
|
+
continue;
|
|
7666
|
+
const parent = ref.getParent();
|
|
7667
|
+
const refSymbol = parent && Node.isShorthandPropertyAssignment(parent)
|
|
7668
|
+
? checker.getShorthandAssignmentValueSymbol(parent.compilerNode)
|
|
7669
|
+
: ref.getSymbol()?.compilerSymbol;
|
|
7670
|
+
if (refSymbol !== symbol)
|
|
7671
|
+
continue;
|
|
7672
|
+
if (this.inSendNothingTest(ref, scope))
|
|
7673
|
+
continue;
|
|
7674
|
+
const value = this.bodyValueAround(ref, scope);
|
|
7675
|
+
if (value === ref) {
|
|
7676
|
+
sent = true;
|
|
7677
|
+
continue;
|
|
7678
|
+
}
|
|
7679
|
+
if (value)
|
|
7680
|
+
return undefined;
|
|
7681
|
+
if (parent && Node.isCallExpression(parent) && parent.getArguments().includes(ref)) {
|
|
7682
|
+
const onward = this.calleeBodyRead(ref, depth + 1);
|
|
7683
|
+
if (onward.kind === 'passes') {
|
|
7684
|
+
sent = true;
|
|
7685
|
+
continue;
|
|
7686
|
+
}
|
|
7687
|
+
if (onward.kind !== 'none')
|
|
7688
|
+
return undefined;
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7691
|
+
return sent ? name : undefined;
|
|
7692
|
+
}
|
|
7173
7693
|
/** `value` is `use` behind what `sentValue` sees through. */
|
|
7174
7694
|
isSameValue(value, use) {
|
|
7175
7695
|
return this.sentValue(value) === use;
|
|
@@ -7442,6 +7962,15 @@ export class TypeInferrer {
|
|
|
7442
7962
|
* carrying the provenance the reading recorded.
|
|
7443
7963
|
*/
|
|
7444
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
|
+
}
|
|
7445
7974
|
const inferred = this.createInferredType(request, contract.text, true, location, undefined, undefined);
|
|
7446
7975
|
if (contract.provenance && contract.provenance.length > 0) {
|
|
7447
7976
|
inferred.any_provenance = contract.provenance;
|
|
@@ -7460,12 +7989,74 @@ export class TypeInferrer {
|
|
|
7460
7989
|
* own locator authoritative when a route declares nothing.
|
|
7461
7990
|
*/
|
|
7462
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
|
+
}
|
|
7463
8002
|
const annotated = this.requestBodyFromHandlerParams(handler);
|
|
7464
8003
|
if (annotated) {
|
|
7465
8004
|
return { text: annotated };
|
|
7466
8005
|
}
|
|
7467
|
-
|
|
7468
|
-
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));
|
|
7469
8060
|
}
|
|
7470
8061
|
/**
|
|
7471
8062
|
* Anchor (a): the request contract declared on the handler's own signature.
|
|
@@ -7678,7 +8269,7 @@ export class TypeInferrer {
|
|
|
7678
8269
|
return;
|
|
7679
8270
|
const text = this.structuralTextFromTypeNode(typeNode);
|
|
7680
8271
|
if (text)
|
|
7681
|
-
found.push({ rank, text, at, how });
|
|
8272
|
+
found.push({ rank, text, at, how, typeNode });
|
|
7682
8273
|
};
|
|
7683
8274
|
for (let i = 0; i < positions.length; i++) {
|
|
7684
8275
|
const position = positions[i];
|
|
@@ -7765,7 +8356,7 @@ export class TypeInferrer {
|
|
|
7765
8356
|
return null;
|
|
7766
8357
|
}
|
|
7767
8358
|
const [first] = decided;
|
|
7768
|
-
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 };
|
|
7769
8360
|
}
|
|
7770
8361
|
/**
|
|
7771
8362
|
* What a call that takes the value at argument `index` states for it:
|
|
@@ -8990,3 +9581,12 @@ export class TypeInferrer {
|
|
|
8990
9581
|
console.error(`[sidecar:type-inferrer:error] ${message}`);
|
|
8991
9582
|
}
|
|
8992
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
|
+
}
|