carrick 0.3.115 → 0.3.116
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/bundler.d.ts +17 -1
- package/sidecar/dist/src/bundler.js +1 -1
- package/sidecar/dist/src/capture/anchors.js +8 -1
- package/sidecar/dist/src/capture/api.d.ts +7 -2
- package/sidecar/dist/src/capture/check-classify.d.ts +3 -1
- package/sidecar/dist/src/capture/check-classify.js +47 -3
- package/sidecar/dist/src/capture/check-deep.d.ts +11 -1
- package/sidecar/dist/src/capture/check-deep.js +14 -1
- package/sidecar/dist/src/capture/check-probe.d.ts +29 -1
- package/sidecar/dist/src/capture/check-probe.js +50 -1
- package/sidecar/dist/src/capture/check.js +20 -3
- package/sidecar/dist/src/capture/index.d.ts +1 -1
- package/sidecar/dist/src/capture/index.js +35 -5
- package/sidecar/dist/src/capture/status-table.d.ts +17 -0
- package/sidecar/dist/src/capture/status-table.js +26 -0
- package/sidecar/dist/src/index.js +1 -1
- package/sidecar/dist/src/symbol-relation.d.ts +36 -0
- package/sidecar/dist/src/symbol-relation.js +109 -0
- package/sidecar/dist/src/type-inferrer.d.ts +118 -3
- package/sidecar/dist/src/type-inferrer.js +481 -18
- package/sidecar/dist/src/types.d.ts +38 -0
- package/sidecar/dist/src/unwidened.js +22 -0
- package/sidecar/dist/src/validators.d.ts +58 -12
- package/sidecar/dist/src/validators.js +7 -0
|
@@ -21,6 +21,8 @@ import * as path from 'node:path';
|
|
|
21
21
|
import { Node, SyntaxKind, VariableDeclarationKind, ts, } from 'ts-morph';
|
|
22
22
|
import { typeToStringAsDeclared } from './print-type.js';
|
|
23
23
|
import { validateInferRequestItem } from './validators.js';
|
|
24
|
+
import { declarationSite } from './bundler.js';
|
|
25
|
+
import { printedTypeHoldsTopType, relateSymbolToRead } from './symbol-relation.js';
|
|
24
26
|
import { notePrintedType, PrintedTypes } from './printed-names.js';
|
|
25
27
|
import { externalImportsOf, isExternalOrigin } from './origin.js';
|
|
26
28
|
import { reachedOnlyOnFailure } from './failure-path.js';
|
|
@@ -28,7 +30,7 @@ import { readResponseModes } from './response-modes.js';
|
|
|
28
30
|
import { functionAtLine } from './function-line-index.js';
|
|
29
31
|
import { endLineOf, startLineOf } from './line-index.js';
|
|
30
32
|
import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
|
|
31
|
-
import { classifyStatusCodes, constituents, httpStatus, statesNoBody, statusKeyCode, statusTableOf, successEntries, } from './capture/index.js';
|
|
33
|
+
import { classifyStatusCodes, constituents, httpStatus, statesNoBody, statesNoShape, statusKeyCode, statusTableOf, successEntries, } from './capture/index.js';
|
|
32
34
|
import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
|
|
33
35
|
import { expandTypeStructural, jsonWireType, } from './type-structural-expander.js';
|
|
34
36
|
/**
|
|
@@ -240,6 +242,12 @@ const BODY_MEMBER_NAMES = new Set(['data', 'body', 'json']);
|
|
|
240
242
|
* JSON, or as raw text (carrick#1842).
|
|
241
243
|
*/
|
|
242
244
|
const WHOLE_BODY_READS = new Set(['json', 'text']);
|
|
245
|
+
/**
|
|
246
|
+
* The other zero-argument reads the platform `Response` takes its whole body
|
|
247
|
+
* out with, in a form that is neither JSON nor text. The expression that
|
|
248
|
+
* holds one holds the body the call answered with (carrick#2112).
|
|
249
|
+
*/
|
|
250
|
+
const BINARY_BODY_READS = new Set(['arrayBuffer', 'blob', 'bytes', 'formData']);
|
|
243
251
|
/** The body format a caller names to read a response as raw text (carrick#1842). */
|
|
244
252
|
const RAW_TEXT_FORMAT = 'text';
|
|
245
253
|
/**
|
|
@@ -415,8 +423,9 @@ export class TypeInferrer {
|
|
|
415
423
|
* @param onRequestDone - Called once per request, as the batch is done with it
|
|
416
424
|
* @returns InferResult with inferred types or errors
|
|
417
425
|
*/
|
|
418
|
-
infer(requests, extractionConfig, onRequestDone) {
|
|
426
|
+
infer(requests, extractionConfig, onRequestDone, modelSymbols) {
|
|
419
427
|
const started = performance.now();
|
|
428
|
+
this.modelSymbols = new Map((modelSymbols ?? []).map((symbol) => [symbol.alias, symbol]));
|
|
420
429
|
const inferredTypes = [];
|
|
421
430
|
const errors = [];
|
|
422
431
|
/** Response inferences the unwidened reading re-reads (carrick#1516). */
|
|
@@ -500,6 +509,74 @@ export class TypeInferrer {
|
|
|
500
509
|
errors: errors.length > 0 ? errors : undefined,
|
|
501
510
|
};
|
|
502
511
|
}
|
|
512
|
+
/** The types the model named, by the alias of the row each is for (carrick#2111). */
|
|
513
|
+
modelSymbols = new Map();
|
|
514
|
+
/**
|
|
515
|
+
* carrick#2111: where the model named a type for the response this request
|
|
516
|
+
* reads, record on `inferred` how that type relates to the compiler's type
|
|
517
|
+
* of what the handler sends (`read`, one type per surviving branch). The
|
|
518
|
+
* driver keeps, re-depths or drops the model's symbol on the answer.
|
|
519
|
+
*
|
|
520
|
+
* Nothing is recorded, and the symbol stands, where there is nothing to
|
|
521
|
+
* compare: the symbol does not resolve in this program, or the published
|
|
522
|
+
* read holds `any` or `unknown` somewhere.
|
|
523
|
+
*/
|
|
524
|
+
relateModelSymbol(request, inferred, read) {
|
|
525
|
+
if (request.infer_kind !== 'response_body' && request.infer_kind !== 'function_return')
|
|
526
|
+
return;
|
|
527
|
+
const named = this.modelSymbols.get(inferred.alias);
|
|
528
|
+
if (!named)
|
|
529
|
+
return;
|
|
530
|
+
const types = read.filter((type) => type !== undefined);
|
|
531
|
+
if (types.length !== read.length || types.length === 0)
|
|
532
|
+
return;
|
|
533
|
+
if (printedTypeHoldsTopType(inferred.type_string))
|
|
534
|
+
return;
|
|
535
|
+
try {
|
|
536
|
+
const symbolType = this.modelSymbolType(named);
|
|
537
|
+
if (!symbolType)
|
|
538
|
+
return;
|
|
539
|
+
const answer = relateSymbolToRead(this.project.getTypeChecker().compilerObject, symbolType.compilerType, types.map((type) => type.compilerType));
|
|
540
|
+
if (answer)
|
|
541
|
+
inferred.model_symbol_relation = answer;
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
// The relation only ever adds an answer; it never costs one.
|
|
545
|
+
this.logError(`Model symbol relation failed for ${inferred.alias}: ${err instanceof Error ? err.message : String(err)}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* `func` is the function the row's line names: it opens on that line (a
|
|
550
|
+
* method under its decorators, an exported handler), or it is the handler
|
|
551
|
+
* of the registration that opens there (`app.post<…>(\n '/path',\n async
|
|
552
|
+
* (request) => { … })`, or a named handler declared elsewhere).
|
|
553
|
+
*/
|
|
554
|
+
isTheRowsOwnFunction(request, func) {
|
|
555
|
+
if (startLineOf(func) === request.line_number)
|
|
556
|
+
return true;
|
|
557
|
+
return this.registrationAtLine(func.getSourceFile(), request.line_number)?.handler === func;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* The declared type of a symbol the model named, found as the bundle finds
|
|
561
|
+
* it (through a barrel's re-exports), in a file this program already
|
|
562
|
+
* holds. A name that declares a value and no type resolves to nothing.
|
|
563
|
+
*/
|
|
564
|
+
modelSymbolType(named) {
|
|
565
|
+
const absolutePath = path.isAbsolute(named.source_file)
|
|
566
|
+
? named.source_file
|
|
567
|
+
: path.resolve(this.repoRoot, named.source_file);
|
|
568
|
+
const sourceFile = this.project.getSourceFile(absolutePath);
|
|
569
|
+
if (!sourceFile)
|
|
570
|
+
return undefined;
|
|
571
|
+
const site = declarationSite(sourceFile, named.symbol_name, named.source_file, this.expandOrigin());
|
|
572
|
+
if ('reason' in site)
|
|
573
|
+
return undefined;
|
|
574
|
+
const declaration = site.sourceFile.getInterface(site.name) ??
|
|
575
|
+
site.sourceFile.getTypeAlias(site.name) ??
|
|
576
|
+
site.sourceFile.getClass(site.name) ??
|
|
577
|
+
site.sourceFile.getEnum(site.name);
|
|
578
|
+
return declaration?.getType();
|
|
579
|
+
}
|
|
503
580
|
/**
|
|
504
581
|
* carrick#1836: list on `result` the declarations behind the names its text
|
|
505
582
|
* prints that the request's file cannot resolve (`PrintedTypes.namesIn`).
|
|
@@ -915,6 +992,16 @@ export class TypeInferrer {
|
|
|
915
992
|
if (provenance && provenance.length > 0) {
|
|
916
993
|
inferred.any_provenance = provenance;
|
|
917
994
|
}
|
|
995
|
+
// The relation is asked only of the function the row's own line names.
|
|
996
|
+
// A locator that binds inside a callback of the handler (`user` in
|
|
997
|
+
// `this.users.find((user) => user.id === id)`) reaches this with that
|
|
998
|
+
// callback, whose return is not the route's, and a read of the wrong
|
|
999
|
+
// function must never displace the symbol the model named.
|
|
1000
|
+
if (this.modelSymbols.has(inferred.alias) && this.isTheRowsOwnFunction(request, func)) {
|
|
1001
|
+
this.relateModelSymbol(request, inferred, [
|
|
1002
|
+
unwrapResult.wasUnwrapped ? unwrapResult.payloadType : awaitedType,
|
|
1003
|
+
]);
|
|
1004
|
+
}
|
|
918
1005
|
return inferred;
|
|
919
1006
|
}
|
|
920
1007
|
/**
|
|
@@ -1201,7 +1288,11 @@ export class TypeInferrer {
|
|
|
1201
1288
|
typeString = this.expandResolvedTypeStructural(resolved, typeText(resolved, payloadNode), this.wireFormatFor(request));
|
|
1202
1289
|
}
|
|
1203
1290
|
const anchor = this.unwrapArrayLevels(this.unwrapPromiseType(payloadType));
|
|
1204
|
-
|
|
1291
|
+
const inferred = this.createInferredType(request, typeString, false, this.getNodeLocation(payloadNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth, this.primaryTypeSymbolSource(anchor.element));
|
|
1292
|
+
this.relateModelSymbol(request, inferred, [
|
|
1293
|
+
unwrapResult.wasUnwrapped ? unwrapResult.payloadType : this.unwrapPromiseType(payloadType),
|
|
1294
|
+
]);
|
|
1295
|
+
return inferred;
|
|
1205
1296
|
}
|
|
1206
1297
|
/**
|
|
1207
1298
|
* The handler a module exports on `line`: a function that opens on exactly
|
|
@@ -1456,6 +1547,7 @@ export class TypeInferrer {
|
|
|
1456
1547
|
const inferred = this.createInferredType(request, recovered.typeString, recovered.isExplicit, this.getNodeLocation(recovered.node), undefined, resolvedSymbol ?? writtenAnchor?.symbol, writtenAnchor ? writtenAnchor.depth : recoveredAnchor?.depth, writtenAnchor ? writtenAnchor.source : resolvedSource);
|
|
1457
1548
|
if (recovered.responseModes)
|
|
1458
1549
|
inferred.response_modes = recovered.responseModes;
|
|
1550
|
+
this.relateModelSymbol(request, inferred, recovered.readTypes);
|
|
1459
1551
|
return inferred;
|
|
1460
1552
|
}
|
|
1461
1553
|
/**
|
|
@@ -2215,9 +2307,25 @@ export class TypeInferrer {
|
|
|
2215
2307
|
if (requestType) {
|
|
2216
2308
|
return this.declaredRequestInferredType(request, requestType, this.getNodeLocation(registrationHandler));
|
|
2217
2309
|
}
|
|
2218
|
-
//
|
|
2219
|
-
//
|
|
2220
|
-
//
|
|
2310
|
+
// carrick#827: the handler reads nothing from its parameters, so the
|
|
2311
|
+
// route takes no request body, and that is a decision. Left undecided,
|
|
2312
|
+
// the capture re-reads this same span and publishes the registration
|
|
2313
|
+
// call's own type (`void` wherever the server library resolves) as the
|
|
2314
|
+
// route's request contract.
|
|
2315
|
+
if (this.handlerReadsNothingFromItsParameters(registrationHandler)) {
|
|
2316
|
+
this.log(`Route registration at ${request.file_path}:${request.line_number}: its handler reads ` +
|
|
2317
|
+
'nothing from its parameters; the route takes no request body');
|
|
2318
|
+
return this.decidedAbstain(request, this.getNodeLocation(registrationHandler), {
|
|
2319
|
+
path: '',
|
|
2320
|
+
kind: 'unknown',
|
|
2321
|
+
reason: 'no_request_body',
|
|
2322
|
+
detail: "the route's handler reads nothing from its parameters (each use of one is a call " +
|
|
2323
|
+
'whose value it discards or returns), so the route takes no request body',
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
// A handler that reads from a parameter and states no type at the read:
|
|
2327
|
+
// do NOT fall through to read the registration call's return type, which
|
|
2328
|
+
// would emit a spurious framework type. Report unresolved, as before.
|
|
2221
2329
|
this.log(`Route registration at ${request.file_path}:${request.line_number} has no typed request read; leaving unresolved`);
|
|
2222
2330
|
return null;
|
|
2223
2331
|
}
|
|
@@ -2475,7 +2583,20 @@ export class TypeInferrer {
|
|
|
2475
2583
|
if (chainRead) {
|
|
2476
2584
|
return { terminal: chainRead, projectionOnly: false, projections: [] };
|
|
2477
2585
|
}
|
|
2478
|
-
|
|
2586
|
+
// A return statement answers only for a call of the function it returns
|
|
2587
|
+
// from (carrick#2282). The nearest `return` above a call in a callback is
|
|
2588
|
+
// an OUTER function's, and its expression is what that function hands
|
|
2589
|
+
// back with the callback inside it: a command table, a client object, a
|
|
2590
|
+
// UI element. The one callback it does answer for is the executor of a
|
|
2591
|
+
// promise it returns (`return new Promise<T>((resolve) => { request(…,
|
|
2592
|
+
// (res) => resolve(…)) })`): what the caller receives is what that
|
|
2593
|
+
// promise settles with, and the call made in the executor produces it.
|
|
2594
|
+
const nearestReturn = callExpr.getFirstAncestorByKind(SyntaxKind.ReturnStatement);
|
|
2595
|
+
const returnStmt = nearestReturn &&
|
|
2596
|
+
(this.findContainingFunctionForNode(nearestReturn) === func ||
|
|
2597
|
+
this.callIsInReturnedPromiseExecutor(nearestReturn, func))
|
|
2598
|
+
? nearestReturn
|
|
2599
|
+
: undefined;
|
|
2479
2600
|
if (returnStmt) {
|
|
2480
2601
|
const returnExpr = returnStmt.getExpression();
|
|
2481
2602
|
if (returnExpr) {
|
|
@@ -2505,6 +2626,15 @@ export class TypeInferrer {
|
|
|
2505
2626
|
const projections = [];
|
|
2506
2627
|
/** The value itself was read: returned, passed, aliased, or body-read. */
|
|
2507
2628
|
let sawWholeRead = false;
|
|
2629
|
+
// Where the call's own value states no type, a call that takes it whole
|
|
2630
|
+
// is the first place the source states one, and the walk follows it
|
|
2631
|
+
// (`parse(raw)` after `const raw = await readRaw()`, carrick#1375).
|
|
2632
|
+
const callValue = this.unwrapPromiseType(callExpr.getType());
|
|
2633
|
+
const callValueIsUntyped = callValue.isAny() || callValue.isUnknown();
|
|
2634
|
+
// A transport response handed whole to a helper is a body read through
|
|
2635
|
+
// that helper (`const data: T = await parseJson(response)`), as
|
|
2636
|
+
// `response.json()` is one in place.
|
|
2637
|
+
const callValueIsTransport = this.typeIsOrContainsResponseMachinery(callValue);
|
|
2508
2638
|
const startPos = callExpr.getStart();
|
|
2509
2639
|
const candidates = this.collectDefUseNodes(func);
|
|
2510
2640
|
for (const expr of candidates) {
|
|
@@ -2582,25 +2712,113 @@ export class TypeInferrer {
|
|
|
2582
2712
|
}
|
|
2583
2713
|
}
|
|
2584
2714
|
}
|
|
2585
|
-
//
|
|
2586
|
-
//
|
|
2587
|
-
// of it is a projection and states a part of the
|
|
2588
|
-
// payload: the hook that derives
|
|
2589
|
-
// result published a boolean map as
|
|
2590
|
-
// whose envelope the client declares
|
|
2715
|
+
// A later expression answers for the call only where it RESTATES the
|
|
2716
|
+
// value: an alias, a cast, a typed binding. An expression that only
|
|
2717
|
+
// reads MEMBERS out of it is a projection and states a part of the
|
|
2718
|
+
// payload, never the payload: the hook that derives
|
|
2719
|
+
// `query.data?.flags` off a query result published a boolean map as
|
|
2720
|
+
// the expected response of a call whose envelope the client declares
|
|
2721
|
+
// in full (carrick#1375). Any other use computes something else from
|
|
2722
|
+
// the value (`describe(page)`, `Math.max(...items.map(…))`,
|
|
2723
|
+
// `rows.map(…)`, `flag && render(row)`): it reads the value whole, and
|
|
2724
|
+
// the answer stays the call's own (carrick#2112). An expression that
|
|
2725
|
+
// takes the body out of a transport response in a form that is not
|
|
2726
|
+
// JSON or text (`new Uint8Array(await res.arrayBuffer())`) holds the
|
|
2727
|
+
// body, and answers as it did.
|
|
2591
2728
|
if (this.expressionUsesNames(expr, currentNames) &&
|
|
2592
2729
|
!this.usesNamesOnlyByProjection(expr, currentNames)) {
|
|
2593
|
-
|
|
2730
|
+
if (callValueIsUntyped ||
|
|
2731
|
+
this.restatesTrackedValue(expr, currentNames) ||
|
|
2732
|
+
this.holdsBinaryBodyRead(expr, currentNames) ||
|
|
2733
|
+
(callValueIsTransport && this.handsTrackedValueToACall(expr, currentNames))) {
|
|
2734
|
+
lastNode = expr;
|
|
2735
|
+
}
|
|
2736
|
+
else {
|
|
2737
|
+
sawWholeRead = true;
|
|
2738
|
+
}
|
|
2594
2739
|
}
|
|
2595
2740
|
}
|
|
2596
2741
|
return {
|
|
2597
|
-
terminal: lastNode,
|
|
2742
|
+
terminal: this.destructuredValueTerminal(lastNode, binding.node, callExpr, callValueIsTransport),
|
|
2598
2743
|
projectionOnly: projections.length > 0 && !sawWholeRead,
|
|
2599
2744
|
projections,
|
|
2600
2745
|
};
|
|
2601
2746
|
}
|
|
2602
2747
|
return { terminal: callExpr, projectionOnly: false, projections: [] };
|
|
2603
2748
|
}
|
|
2749
|
+
/**
|
|
2750
|
+
* Where the walk ends on the call's own destructuring binding (`const {
|
|
2751
|
+
* tasks } = await api.list()`), the declaration has no type of its own: the
|
|
2752
|
+
* checker answers `any` for it. The value it takes apart is the call's, so
|
|
2753
|
+
* the call answers (carrick#2112). Not for a transport value: a helper's
|
|
2754
|
+
* `{ body, response }` read as text would bypass the capture's machinery
|
|
2755
|
+
* guard, so that binding stays unread and the capture answers the row.
|
|
2756
|
+
*/
|
|
2757
|
+
destructuredValueTerminal(lastNode, bindingNode, callExpr, callValueIsTransport) {
|
|
2758
|
+
if (lastNode !== bindingNode ||
|
|
2759
|
+
callValueIsTransport ||
|
|
2760
|
+
!Node.isVariableDeclaration(lastNode) ||
|
|
2761
|
+
Node.isIdentifier(lastNode.getNameNode())) {
|
|
2762
|
+
return lastNode;
|
|
2763
|
+
}
|
|
2764
|
+
return callExpr;
|
|
2765
|
+
}
|
|
2766
|
+
/**
|
|
2767
|
+
* `expr` declares a name for what a call returns when it is handed a
|
|
2768
|
+
* tracked name whole (`const data: T = await parseJson(response, 'op')`).
|
|
2769
|
+
*/
|
|
2770
|
+
handsTrackedValueToACall(expr, names) {
|
|
2771
|
+
if (!Node.isVariableDeclaration(expr))
|
|
2772
|
+
return false;
|
|
2773
|
+
const init = this.unwrapExpressionNode(expr.getInitializer());
|
|
2774
|
+
return (Node.isCallExpression(init) &&
|
|
2775
|
+
init
|
|
2776
|
+
.getArguments()
|
|
2777
|
+
.some((argument) => this.isIdentifierUsage(this.unwrapExpressionNode(argument), names)));
|
|
2778
|
+
}
|
|
2779
|
+
/** `expr` holds a binary whole-body read taken on a tracked name. */
|
|
2780
|
+
holdsBinaryBodyRead(expr, names) {
|
|
2781
|
+
return expr
|
|
2782
|
+
.getDescendantsOfKind(SyntaxKind.Identifier)
|
|
2783
|
+
.some((id) => this.isIdentifierUsage(id, names) &&
|
|
2784
|
+
this.bodyReadOnReceiver(id, BINARY_BODY_READS) !== undefined);
|
|
2785
|
+
}
|
|
2786
|
+
/**
|
|
2787
|
+
* `func`, the function a call is made in, is the executor of the promise
|
|
2788
|
+
* `returnStmt` returns: `return new Promise<T>((resolve, reject) => { … })`
|
|
2789
|
+
* (carrick#2282). The promise is recognised by the language's await
|
|
2790
|
+
* protocol on the constructed value, never by a name.
|
|
2791
|
+
*/
|
|
2792
|
+
callIsInReturnedPromiseExecutor(returnStmt, func) {
|
|
2793
|
+
if (!func || !Node.isReturnStatement(returnStmt))
|
|
2794
|
+
return false;
|
|
2795
|
+
const returned = this.unwrapExpressionNode(returnStmt.getExpression());
|
|
2796
|
+
if (!Node.isNewExpression(returned))
|
|
2797
|
+
return false;
|
|
2798
|
+
const [executor] = returned.getArguments();
|
|
2799
|
+
if (!executor || this.unwrapExpressionNode(executor) !== func)
|
|
2800
|
+
return false;
|
|
2801
|
+
const constructed = returned.getType();
|
|
2802
|
+
return this.unwrapPromiseType(constructed) !== constructed;
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* `expr` declares a name for the tracked value as it is: its initializer,
|
|
2806
|
+
* with the wrappers that leave a value what it was peeled (parentheses,
|
|
2807
|
+
* `await`, `as`, `satisfies`, `!`), is a tracked name (carrick#2112). The
|
|
2808
|
+
* declaration's type is then the value's, as the source restates it.
|
|
2809
|
+
*/
|
|
2810
|
+
restatesTrackedValue(expr, names) {
|
|
2811
|
+
if (!Node.isVariableDeclaration(expr))
|
|
2812
|
+
return false;
|
|
2813
|
+
let value = expr.getInitializer();
|
|
2814
|
+
for (;;) {
|
|
2815
|
+
const peeled = value && this.unwrapExpressionNode(value);
|
|
2816
|
+
value = peeled && Node.isSatisfiesExpression(peeled) ? peeled.getExpression() : peeled;
|
|
2817
|
+
if (value === peeled)
|
|
2818
|
+
break;
|
|
2819
|
+
}
|
|
2820
|
+
return !!value && this.isIdentifierUsage(value, names);
|
|
2821
|
+
}
|
|
2604
2822
|
/**
|
|
2605
2823
|
* True when a member read on the call's result resolves to one of that
|
|
2606
2824
|
* result's own TYPE ARGUMENTS — the source is unwrapping a generic envelope
|
|
@@ -2668,6 +2886,10 @@ export class TypeInferrer {
|
|
|
2668
2886
|
* as written.
|
|
2669
2887
|
*/
|
|
2670
2888
|
resultCarrierPayload(type, at, projections, where, callArgs) {
|
|
2889
|
+
const deferred = this.deferredConditionalPayload(type, at, projections, where, callArgs);
|
|
2890
|
+
if (deferred !== undefined) {
|
|
2891
|
+
return deferred;
|
|
2892
|
+
}
|
|
2671
2893
|
const shape = this.resultCarrierArguments(type, at, callArgs);
|
|
2672
2894
|
if (!shape) {
|
|
2673
2895
|
return undefined;
|
|
@@ -2736,6 +2958,90 @@ export class TypeInferrer {
|
|
|
2736
2958
|
'only non-error one and the source reads none of them out. Not guessing which ' +
|
|
2737
2959
|
'argument is the payload');
|
|
2738
2960
|
}
|
|
2961
|
+
/**
|
|
2962
|
+
* carrick#1841: the branches of a conditional type the compiler left
|
|
2963
|
+
* deferred on a type parameter, each with its promise-like peeled, or
|
|
2964
|
+
* `undefined` for any other type.
|
|
2965
|
+
*
|
|
2966
|
+
* Inside a generic function whose result depends on a type parameter it has
|
|
2967
|
+
* not fixed (`<Throw extends boolean>(o) => client.get<Table, Errors,
|
|
2968
|
+
* Throw>(…)`), the call's result is such a conditional. The compiler states
|
|
2969
|
+
* what its branches allow itself: the constraint of the conditional, which
|
|
2970
|
+
* is its apparent type. No branch is read off the alias's declaration.
|
|
2971
|
+
*/
|
|
2972
|
+
deferredConditionalBranches(type) {
|
|
2973
|
+
const compilerType = type.compilerType;
|
|
2974
|
+
if (!(compilerType.flags & ts.TypeFlags.Conditional))
|
|
2975
|
+
return undefined;
|
|
2976
|
+
const checked = compilerType.checkType;
|
|
2977
|
+
if (!(checked.flags & ts.TypeFlags.TypeParameter))
|
|
2978
|
+
return undefined;
|
|
2979
|
+
let apparent;
|
|
2980
|
+
try {
|
|
2981
|
+
apparent = type.getApparentType();
|
|
2982
|
+
}
|
|
2983
|
+
catch {
|
|
2984
|
+
return undefined;
|
|
2985
|
+
}
|
|
2986
|
+
if (apparent === type || apparent.isUnknown() || apparent.isAny())
|
|
2987
|
+
return undefined;
|
|
2988
|
+
const members = apparent.isUnion() ? apparent.getUnionTypes() : [apparent];
|
|
2989
|
+
return members.map((member) => this.unwrapThenableType(this.unwrapPromiseType(member)));
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* carrick#1841: what a call result deferred on a type parameter carries,
|
|
2993
|
+
* read through its branches. `undefined` where `type` is no such
|
|
2994
|
+
* conditional, or none of its branches is a result carrier, and the type
|
|
2995
|
+
* keeps the answer it had.
|
|
2996
|
+
*
|
|
2997
|
+
* Each branch that is a carrier is read by the carrier read, with the
|
|
2998
|
+
* call's own type arguments. Every one of them must name the same payload,
|
|
2999
|
+
* by the compiler's type identity, and every other branch must hold that
|
|
3000
|
+
* payload as the type of one of its members (the branch a client answers
|
|
3001
|
+
* with when it throws on failure holds the body and no error side). Then
|
|
3002
|
+
* the payload is the body whichever branch the caller fixes. A carrier
|
|
3003
|
+
* branch that states no contract decides for the whole. Branches that
|
|
3004
|
+
* disagree state none: the conditional is the client's bookkeeping and is
|
|
3005
|
+
* never published as written.
|
|
3006
|
+
*/
|
|
3007
|
+
deferredConditionalPayload(type, at, projections, where, callArgs) {
|
|
3008
|
+
const branches = this.deferredConditionalBranches(type);
|
|
3009
|
+
if (!branches)
|
|
3010
|
+
return undefined;
|
|
3011
|
+
const reads = branches.map((branch) => ({
|
|
3012
|
+
branch,
|
|
3013
|
+
read: this.resultCarrierPayload(branch, at, projections, where, callArgs),
|
|
3014
|
+
}));
|
|
3015
|
+
const carriers = reads.flatMap((entry) => (entry.read ? [entry.read] : []));
|
|
3016
|
+
if (carriers.length === 0)
|
|
3017
|
+
return undefined;
|
|
3018
|
+
const abstained = carriers.find((read) => 'abstain' in read);
|
|
3019
|
+
if (abstained)
|
|
3020
|
+
return abstained;
|
|
3021
|
+
const [{ payload }] = carriers;
|
|
3022
|
+
const isPayload = (candidate) => candidate.compilerType === payload.compilerType;
|
|
3023
|
+
if (!carriers.every((read) => isPayload(read.payload))) {
|
|
3024
|
+
return {
|
|
3025
|
+
abstain: "the call's result depends on a type parameter and its branches carry different " +
|
|
3026
|
+
'bodies, so this site states no response contract',
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
const holdsPayload = (branch) => branch.getProperties().some((property) => {
|
|
3030
|
+
try {
|
|
3031
|
+
return isPayload(property.getTypeAtLocation(at));
|
|
3032
|
+
}
|
|
3033
|
+
catch {
|
|
3034
|
+
return false;
|
|
3035
|
+
}
|
|
3036
|
+
});
|
|
3037
|
+
if (!reads.every((entry) => entry.read !== undefined || holdsPayload(entry.branch))) {
|
|
3038
|
+
return {
|
|
3039
|
+
abstain: "the call's result depends on a type parameter and one of its branches does not " +
|
|
3040
|
+
'hold the body the others carry, so this site states no response contract',
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
return { payload };
|
|
3044
|
+
}
|
|
2739
3045
|
/**
|
|
2740
3046
|
* The value of a response table's one success row, for a table a carrier
|
|
2741
3047
|
* holds as itself rather than through its body. `undefined` where the table
|
|
@@ -2788,6 +3094,17 @@ export class TypeInferrer {
|
|
|
2788
3094
|
* holds as a member. `undefined` when `type` is not a carrier.
|
|
2789
3095
|
*/
|
|
2790
3096
|
resultCarrierArguments(type, at, callArgs = []) {
|
|
3097
|
+
// carrick#1841: a conditional deferred on a type parameter is a carrier
|
|
3098
|
+
// where one of its branches is.
|
|
3099
|
+
const deferredBranches = this.deferredConditionalBranches(type);
|
|
3100
|
+
if (deferredBranches) {
|
|
3101
|
+
for (const branch of deferredBranches) {
|
|
3102
|
+
const shape = this.resultCarrierArguments(branch, at, callArgs);
|
|
3103
|
+
if (shape)
|
|
3104
|
+
return shape;
|
|
3105
|
+
}
|
|
3106
|
+
return undefined;
|
|
3107
|
+
}
|
|
2791
3108
|
const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
|
|
2792
3109
|
if (!carrier.isUnion()) {
|
|
2793
3110
|
return undefined;
|
|
@@ -3278,14 +3595,15 @@ export class TypeInferrer {
|
|
|
3278
3595
|
* The zero-argument whole-body read that takes `receiver` as its receiver,
|
|
3279
3596
|
* `res.json()` or `res.text()`, or `undefined`. A text read is a body read
|
|
3280
3597
|
* like a json one (carrick#1842): without it, `return res.text()` left the
|
|
3281
|
-
* walk on the response binding and published the transport object.
|
|
3598
|
+
* walk on the response binding and published the transport object. `reads`
|
|
3599
|
+
* names the reads asked for.
|
|
3282
3600
|
*/
|
|
3283
|
-
bodyReadOnReceiver(receiver) {
|
|
3601
|
+
bodyReadOnReceiver(receiver, reads = WHOLE_BODY_READS) {
|
|
3284
3602
|
const access = receiver.getParent();
|
|
3285
3603
|
if (!access ||
|
|
3286
3604
|
!Node.isPropertyAccessExpression(access) ||
|
|
3287
3605
|
access.getExpression() !== receiver ||
|
|
3288
|
-
!
|
|
3606
|
+
!reads.has(access.getName())) {
|
|
3289
3607
|
return undefined;
|
|
3290
3608
|
}
|
|
3291
3609
|
const call = access.getParent();
|
|
@@ -4126,6 +4444,12 @@ export class TypeInferrer {
|
|
|
4126
4444
|
*/
|
|
4127
4445
|
expandResolvedTypeStructural(type, fallback, wire = 'declared') {
|
|
4128
4446
|
try {
|
|
4447
|
+
// carrick#2116: a narrowed `unknown` or `object`, at the root or as a
|
|
4448
|
+
// member of the root union, states no shape. It would print `{}` or
|
|
4449
|
+
// `object` and be judged as a body that lacks every field.
|
|
4450
|
+
if (statesNoShape(this.project.getTypeChecker().compilerObject, type.compilerType)) {
|
|
4451
|
+
return 'unknown';
|
|
4452
|
+
}
|
|
4129
4453
|
const expanded = expandTypeStructural(type, this.expandOrigin(), { wire });
|
|
4130
4454
|
// A wire print that differs from the declared one is the answer even
|
|
4131
4455
|
// without an inlined object: a bare `Date` payload sends a string.
|
|
@@ -4276,6 +4600,7 @@ export class TypeInferrer {
|
|
|
4276
4600
|
// A union joined several payloads, so there is no single anchor — the
|
|
4277
4601
|
// same rule the wrapper-unwrap path applies.
|
|
4278
4602
|
anchorType: distinct.length === 1 ? distinct[0].anchorType : undefined,
|
|
4603
|
+
readTypes: distinct.map((c) => c.anchorType),
|
|
4279
4604
|
statedTypeNode: distinct.length === 1 ? distinct[0].statedTypeNode : undefined,
|
|
4280
4605
|
};
|
|
4281
4606
|
}
|
|
@@ -5914,6 +6239,130 @@ export class TypeInferrer {
|
|
|
5914
6239
|
}
|
|
5915
6240
|
return undefined;
|
|
5916
6241
|
}
|
|
6242
|
+
/**
|
|
6243
|
+
* True when `handler` reads nothing from the incoming message (carrick#827):
|
|
6244
|
+
* every use of one of its parameters, named or destructured, is the receiver
|
|
6245
|
+
* of a call chain whose value the handler discards or returns
|
|
6246
|
+
* (`reply.json(result)`, `return c.status(201).json(result)`). That is a
|
|
6247
|
+
* send. A returned chain must end in a call that takes an argument, so a
|
|
6248
|
+
* read handed straight back (`return request.json()`) is not one.
|
|
6249
|
+
*
|
|
6250
|
+
* Anything else may be a read of the body and makes this false: a property
|
|
6251
|
+
* read, a call whose value is used, a parameter handed whole to another
|
|
6252
|
+
* function, or a call in the chain handed an argument that can receive what
|
|
6253
|
+
* the parameter gives (`argumentCanReceive`: a callback, an object with a
|
|
6254
|
+
* method), as in `request.pipe(dest)`, `request.on('data', cb)` and
|
|
6255
|
+
* `request.json().then(cb)`. No method or property name is consulted. A
|
|
6256
|
+
* handler with no parameter, or one that uses `this`, proves nothing: it may
|
|
6257
|
+
* read the message from `this` or a closure.
|
|
6258
|
+
*/
|
|
6259
|
+
handlerReadsNothingFromItsParameters(handler) {
|
|
6260
|
+
// A declared `this` parameter, or any `this` in the body, is a way in the
|
|
6261
|
+
// walk below does not follow.
|
|
6262
|
+
const parameters = new Set(handler.getParameters().filter((parameter) => parameter.getName() !== 'this'));
|
|
6263
|
+
if (parameters.size === 0)
|
|
6264
|
+
return false;
|
|
6265
|
+
const body = handler.getBody();
|
|
6266
|
+
if (!body)
|
|
6267
|
+
return false;
|
|
6268
|
+
if (body.getFirstDescendantByKind(SyntaxKind.ThisKeyword))
|
|
6269
|
+
return false;
|
|
6270
|
+
const namesAParameter = (identifier) => (identifier.getSymbol()?.getDeclarations() ?? []).some((declaration) => {
|
|
6271
|
+
const parameter = Node.isParameterDeclaration(declaration)
|
|
6272
|
+
? declaration
|
|
6273
|
+
: Node.isBindingElement(declaration)
|
|
6274
|
+
? declaration.getFirstAncestorByKind(SyntaxKind.Parameter)
|
|
6275
|
+
: undefined;
|
|
6276
|
+
return parameter !== undefined && parameters.has(parameter);
|
|
6277
|
+
});
|
|
6278
|
+
for (const identifier of body.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
6279
|
+
if (!namesAParameter(identifier))
|
|
6280
|
+
continue;
|
|
6281
|
+
// Climb the call chain the parameter is the root receiver of.
|
|
6282
|
+
let top = identifier;
|
|
6283
|
+
let lastCall;
|
|
6284
|
+
for (;;) {
|
|
6285
|
+
const parent = top.getParent();
|
|
6286
|
+
if (!parent)
|
|
6287
|
+
break;
|
|
6288
|
+
if (Node.isPropertyAccessExpression(parent) && parent.getExpression() === top) {
|
|
6289
|
+
top = parent;
|
|
6290
|
+
}
|
|
6291
|
+
else if (Node.isCallExpression(parent) && parent.getExpression() === top) {
|
|
6292
|
+
// A send hands the parameter data; an argument able to receive may
|
|
6293
|
+
// be where the body goes.
|
|
6294
|
+
if (parent.getArguments().some((argument) => this.argumentCanReceive(argument))) {
|
|
6295
|
+
return false;
|
|
6296
|
+
}
|
|
6297
|
+
lastCall = parent;
|
|
6298
|
+
top = parent;
|
|
6299
|
+
}
|
|
6300
|
+
else if ((Node.isAwaitExpression(parent) ||
|
|
6301
|
+
Node.isParenthesizedExpression(parent) ||
|
|
6302
|
+
Node.isNonNullExpression(parent)) &&
|
|
6303
|
+
parent.getExpression() === top) {
|
|
6304
|
+
top = parent;
|
|
6305
|
+
}
|
|
6306
|
+
else {
|
|
6307
|
+
break;
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
// The chain has to end in a call: a property read yields a value.
|
|
6311
|
+
if (!lastCall || this.unwrapExpressionNode(top) !== lastCall)
|
|
6312
|
+
return false;
|
|
6313
|
+
const holder = top.getParent();
|
|
6314
|
+
const discarded = !!holder && Node.isExpressionStatement(holder);
|
|
6315
|
+
const returned = !!holder &&
|
|
6316
|
+
(Node.isReturnStatement(holder) || (holder === handler && handler.getBody() === top)) &&
|
|
6317
|
+
lastCall.getArguments().length > 0;
|
|
6318
|
+
if (!discarded && !returned)
|
|
6319
|
+
return false;
|
|
6320
|
+
}
|
|
6321
|
+
return true;
|
|
6322
|
+
}
|
|
6323
|
+
/**
|
|
6324
|
+
* True unless `argument` is proven to be data that cannot receive anything
|
|
6325
|
+
* from the call it is handed to (carrick#827). Data is a primitive or literal,
|
|
6326
|
+
* `null`/`undefined`, or an object with no call or construct signature and
|
|
6327
|
+
* no callable member; an array of such, or a union all of whose members are.
|
|
6328
|
+
* A callback, an object with a method (a stream, an emitter), or a type the
|
|
6329
|
+
* compiler cannot state (`any`, `unknown`, a type parameter) can receive.
|
|
6330
|
+
* Sibling of `callResultIsPlainData`, which reads a call's result and so
|
|
6331
|
+
* rejects primitives and empty objects; an argument of either is data.
|
|
6332
|
+
*/
|
|
6333
|
+
argumentCanReceive(argument) {
|
|
6334
|
+
const canReceive = (type, depth) => {
|
|
6335
|
+
if (depth > 3)
|
|
6336
|
+
return true;
|
|
6337
|
+
if (type.isAny() || type.isUnknown() || type.isTypeParameter())
|
|
6338
|
+
return true;
|
|
6339
|
+
if (type.isUnion()) {
|
|
6340
|
+
return type.getUnionTypes().some((member) => canReceive(member, depth + 1));
|
|
6341
|
+
}
|
|
6342
|
+
if (type.isIntersection()) {
|
|
6343
|
+
return type.getIntersectionTypes().some((member) => canReceive(member, depth + 1));
|
|
6344
|
+
}
|
|
6345
|
+
const element = type.getArrayElementType();
|
|
6346
|
+
if (element)
|
|
6347
|
+
return canReceive(element, depth + 1);
|
|
6348
|
+
if (type.isTuple()) {
|
|
6349
|
+
return type.getTupleElements().some((member) => canReceive(member, depth + 1));
|
|
6350
|
+
}
|
|
6351
|
+
if (!type.isObject())
|
|
6352
|
+
return false;
|
|
6353
|
+
if (this.isCallableType(type))
|
|
6354
|
+
return true;
|
|
6355
|
+
const indexed = type.getStringIndexType() ?? type.getNumberIndexType();
|
|
6356
|
+
const callable = (held) => held.isAny() ||
|
|
6357
|
+
(held.isUnion()
|
|
6358
|
+
? held.getUnionTypes().some((member) => this.isCallableType(member))
|
|
6359
|
+
: this.isCallableType(held));
|
|
6360
|
+
if (indexed && callable(indexed))
|
|
6361
|
+
return true;
|
|
6362
|
+
return type.getProperties().some((member) => callable(member.getTypeAtLocation(argument)));
|
|
6363
|
+
};
|
|
6364
|
+
return canReceive(argument.getType(), 0);
|
|
6365
|
+
}
|
|
5917
6366
|
/**
|
|
5918
6367
|
* Scan a handler function body for the FIRST request-body read that carries a
|
|
5919
6368
|
* type, and return that type's structural text — or null when the body holds
|
|
@@ -7489,6 +7938,20 @@ export class TypeInferrer {
|
|
|
7489
7938
|
const MIN_REVERSE_MATCH_COVERAGE = 0.5;
|
|
7490
7939
|
const containingMatches = candidates.filter((c) => c.text.includes(normalizedTarget));
|
|
7491
7940
|
if (containingMatches.length > 0) {
|
|
7941
|
+
// A text that is only a wrapped call's first line (`load({`) is held by
|
|
7942
|
+
// every sibling that opens alike, and the smallest of them is not the
|
|
7943
|
+
// one the locator names (carrick#2114). The nodes whose own text OPENS
|
|
7944
|
+
// with the text are the ones a first line names: among those the stated
|
|
7945
|
+
// line decides, then size. A fragment from the middle of a node opens
|
|
7946
|
+
// nothing, and stays on the smallest node holding it.
|
|
7947
|
+
const opening = lineNumber === undefined
|
|
7948
|
+
? []
|
|
7949
|
+
: containingMatches.filter((c) => c.text.startsWith(normalizedTarget));
|
|
7950
|
+
if (lineNumber !== undefined && opening.length > 0) {
|
|
7951
|
+
const distance = (c) => Math.abs(startLineOf(c.node) - lineNumber);
|
|
7952
|
+
const nearest = Math.min(...opening.map(distance));
|
|
7953
|
+
return this.pickBestMatch(opening.filter((c) => distance(c) === nearest).map((c) => c.node), lineNumber);
|
|
7954
|
+
}
|
|
7492
7955
|
return this.pickBestMatch(containingMatches.map((c) => c.node), lineNumber);
|
|
7493
7956
|
}
|
|
7494
7957
|
const reverseMatches = candidates.filter((c) => c.text.length >= MIN_REVERSE_MATCH_LEN &&
|