carrick 0.3.108 → 0.3.110
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 +9 -10
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/bundler.d.ts +10 -0
- package/sidecar/dist/src/bundler.js +42 -0
- package/sidecar/dist/src/capture/anchors.js +32 -0
- package/sidecar/dist/src/capture/api.d.ts +5 -0
- package/sidecar/dist/src/capture/check-deep.js +2 -1
- package/sidecar/dist/src/capture/check-workspace.d.ts +11 -0
- package/sidecar/dist/src/capture/check-workspace.js +81 -8
- package/sidecar/dist/src/capture/compiler-options.d.ts +39 -0
- package/sidecar/dist/src/capture/compiler-options.js +100 -0
- package/sidecar/dist/src/capture/deno-project.js +3 -2
- package/sidecar/dist/src/capture/index.d.ts +2 -0
- package/sidecar/dist/src/capture/index.js +25 -8
- package/sidecar/dist/src/capture/installed-package-directory.d.ts +22 -0
- package/sidecar/dist/src/capture/installed-package-directory.js +30 -0
- package/sidecar/dist/src/capture/installed-package.js +3 -2
- package/sidecar/dist/src/capture/resolution-edges.d.ts +36 -0
- package/sidecar/dist/src/capture/resolution-edges.js +150 -0
- package/sidecar/dist/src/capture/self-check.js +3 -2
- package/sidecar/dist/src/capture/status-table.d.ts +62 -0
- package/sidecar/dist/src/capture/status-table.js +104 -0
- package/sidecar/dist/src/definition-resolver.js +3 -2
- package/sidecar/dist/src/failure-path.d.ts +42 -2
- package/sidecar/dist/src/failure-path.js +38 -13
- package/sidecar/dist/src/index.js +91 -7
- package/sidecar/dist/src/library-claims.js +2 -0
- package/sidecar/dist/src/line-index.d.ts +9 -0
- package/sidecar/dist/src/line-index.js +24 -0
- package/sidecar/dist/src/print-type.d.ts +48 -0
- package/sidecar/dist/src/print-type.js +71 -0
- package/sidecar/dist/src/printed-names.js +2 -1
- package/sidecar/dist/src/program-files.d.ts +44 -0
- package/sidecar/dist/src/program-files.js +64 -0
- package/sidecar/dist/src/project-loader.d.ts +5 -0
- package/sidecar/dist/src/project-loader.js +14 -5
- package/sidecar/dist/src/response-modes.d.ts +77 -0
- package/sidecar/dist/src/response-modes.js +459 -0
- package/sidecar/dist/src/retype.js +7 -1
- package/sidecar/dist/src/type-inferrer.d.ts +53 -0
- package/sidecar/dist/src/type-inferrer.js +267 -79
- package/sidecar/dist/src/types.d.ts +50 -2
- package/sidecar/dist/src/validators.d.ts +159 -88
- package/sidecar/dist/src/validators.js +13 -0
|
@@ -19,12 +19,16 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { Node, SyntaxKind, ts, } from 'ts-morph';
|
|
22
|
+
import { typeToStringAsDeclared } from './print-type.js';
|
|
22
23
|
import { validateInferRequestItem } from './validators.js';
|
|
23
24
|
import { notePrintedType, PrintedTypes } from './printed-names.js';
|
|
24
25
|
import { externalImportsOf, isExternalOrigin } from './origin.js';
|
|
25
26
|
import { reachedOnlyOnFailure } from './failure-path.js';
|
|
27
|
+
import { readResponseModes } from './response-modes.js';
|
|
26
28
|
import { functionAtLine } from './function-line-index.js';
|
|
29
|
+
import { endLineOf, startLineOf } from './line-index.js';
|
|
27
30
|
import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
|
|
31
|
+
import { classifyStatusCodes, constituents, httpStatus, statesNoBody, statusKeyCode, statusTableOf, successEntries, } from './capture/index.js';
|
|
28
32
|
import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
|
|
29
33
|
import { expandTypeStructural, jsonWireType, } from './type-structural-expander.js';
|
|
30
34
|
/**
|
|
@@ -168,11 +172,19 @@ const RESPONSE_HELPER_MAX_DEPTH = 4;
|
|
|
168
172
|
* the branch an error path, whose shape is not the endpoint's contract.
|
|
169
173
|
*/
|
|
170
174
|
const STATUS_MEMBER_NAMES = ['status', 'statusCode'];
|
|
171
|
-
/**
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
/**
|
|
176
|
+
* The items whose whitespace-collapsed texts differ, first of each kept, in
|
|
177
|
+
* order: how a joined union drops a member it already has.
|
|
178
|
+
*/
|
|
179
|
+
function distinctByText(items, text) {
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
return items.filter((item) => {
|
|
182
|
+
const key = text(item).replace(/\s+/g, ' ').trim();
|
|
183
|
+
if (seen.has(key))
|
|
184
|
+
return false;
|
|
185
|
+
seen.add(key);
|
|
186
|
+
return true;
|
|
187
|
+
});
|
|
176
188
|
}
|
|
177
189
|
/**
|
|
178
190
|
* The status codes a TYPE fixes: a numeric literal, or a union made only of
|
|
@@ -192,16 +204,6 @@ function statusCodesOfType(type) {
|
|
|
192
204
|
}
|
|
193
205
|
return codes.length > 0 ? codes : undefined;
|
|
194
206
|
}
|
|
195
|
-
/** One verdict for a set of status codes, or `mixed` when they disagree. */
|
|
196
|
-
function classifyStatusCodes(codes) {
|
|
197
|
-
if (codes.every((code) => code >= 400))
|
|
198
|
-
return 'error';
|
|
199
|
-
if (codes.every((code) => code >= 300 && code < 400))
|
|
200
|
-
return 'redirect';
|
|
201
|
-
if (codes.every((code) => code < 300))
|
|
202
|
-
return 'success';
|
|
203
|
-
return 'mixed';
|
|
204
|
-
}
|
|
205
207
|
/**
|
|
206
208
|
* The parts of a request that ARE the body, as validator middleware names them
|
|
207
209
|
* (`validate('json', Schema)`). HTTP vocabulary for how a body is carried — the
|
|
@@ -242,8 +244,12 @@ const RAW_TEXT_FORMAT = 'text';
|
|
|
242
244
|
const TYPE_TEXT_FLAGS = ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias;
|
|
243
245
|
function typeText(type, enclosingNode) {
|
|
244
246
|
// Timed as the print (carrick#1985): the type is already computed when it
|
|
245
|
-
// gets here, so this is what its length costs.
|
|
246
|
-
|
|
247
|
+
// gets here, so this is what its length costs. A module the print names is
|
|
248
|
+
// written as its declaring file, whichever file it is printed from
|
|
249
|
+
// (carrick#2019).
|
|
250
|
+
const text = timedPhase('print', () => enclosingNode
|
|
251
|
+
? typeToStringAsDeclared(enclosingNode.getProject().getTypeChecker().compilerObject, type.compilerType, enclosingNode.compilerNode, TYPE_TEXT_FLAGS)
|
|
252
|
+
: type.getText(undefined, TYPE_TEXT_FLAGS));
|
|
247
253
|
notePrintedType(type, enclosingNode, text);
|
|
248
254
|
return text;
|
|
249
255
|
}
|
|
@@ -968,7 +974,7 @@ export class TypeInferrer {
|
|
|
968
974
|
Node.isArrowFunction(ancestor) ||
|
|
969
975
|
Node.isFunctionExpression(ancestor) ||
|
|
970
976
|
Node.isMethodDeclaration(ancestor)) &&
|
|
971
|
-
ancestor
|
|
977
|
+
startLineOf(ancestor) === request.line_number);
|
|
972
978
|
const declaredHandler = insideFunctionOnLine
|
|
973
979
|
? this.handlerDeclaredAtLine(sourceFile, request.line_number)
|
|
974
980
|
: undefined;
|
|
@@ -1125,7 +1131,7 @@ export class TypeInferrer {
|
|
|
1125
1131
|
*/
|
|
1126
1132
|
exportedFunctionOpeningOn(sourceFile, line) {
|
|
1127
1133
|
const declared = this.findFunctionByLine(sourceFile, line);
|
|
1128
|
-
if (!declared || declared
|
|
1134
|
+
if (!declared || startLineOf(declared) !== line)
|
|
1129
1135
|
return undefined;
|
|
1130
1136
|
if (Node.isFunctionDeclaration(declared)) {
|
|
1131
1137
|
return Node.isSourceFile(declared.getParent()) && declared.isExported()
|
|
@@ -1294,7 +1300,10 @@ export class TypeInferrer {
|
|
|
1294
1300
|
const resolvedSource = resolvedSymbol !== undefined && recoveredAnchor
|
|
1295
1301
|
? this.primaryTypeSymbolSource(recoveredAnchor.element)
|
|
1296
1302
|
: undefined;
|
|
1297
|
-
|
|
1303
|
+
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);
|
|
1304
|
+
if (recovered.responseModes)
|
|
1305
|
+
inferred.response_modes = recovered.responseModes;
|
|
1306
|
+
return inferred;
|
|
1298
1307
|
}
|
|
1299
1308
|
/**
|
|
1300
1309
|
* carrick#1161: type a route response from every send in the handler the
|
|
@@ -1467,7 +1476,9 @@ export class TypeInferrer {
|
|
|
1467
1476
|
// carrier read gives its own. A result that is no carrier keeps the
|
|
1468
1477
|
// answer it had.
|
|
1469
1478
|
const where = `${request.file_path}:${request.line_number}`;
|
|
1470
|
-
const
|
|
1479
|
+
const callArgs = this.callResultArguments(callExpr);
|
|
1480
|
+
const resultIsCarrier = !explicitType &&
|
|
1481
|
+
this.resultCarrierArguments(returnType, terminalNode, callArgs) !== undefined;
|
|
1471
1482
|
// carrick#1877: the terminal holds a thenable the names do not peel, a
|
|
1472
1483
|
// subclass of `Promise` or a class with a `then` of its own. What a
|
|
1473
1484
|
// caller receives is what `await` yields for it, as it is for the
|
|
@@ -1516,12 +1527,22 @@ export class TypeInferrer {
|
|
|
1516
1527
|
const readThroughCarrier = unwrapResult.wasUnwrapped &&
|
|
1517
1528
|
resultIsCarrier &&
|
|
1518
1529
|
!!afterRules &&
|
|
1519
|
-
this.resultCarrierArguments(afterRules, terminalNode) === undefined;
|
|
1520
|
-
const
|
|
1530
|
+
this.resultCarrierArguments(afterRules, terminalNode, callArgs) === undefined;
|
|
1531
|
+
const carrierRead = explicitType || !afterRules
|
|
1521
1532
|
? undefined
|
|
1522
1533
|
: readThroughCarrier
|
|
1523
|
-
? afterRules
|
|
1524
|
-
: this.resultCarrierPayload(afterRules, terminalNode, use.projections, where);
|
|
1534
|
+
? { payload: afterRules }
|
|
1535
|
+
: this.resultCarrierPayload(afterRules, terminalNode, use.projections, where, callArgs);
|
|
1536
|
+
// carrick#1841: a carrier built from a response table whose success side
|
|
1537
|
+
// states no body (a `204` row), or whose side cannot be told, states no
|
|
1538
|
+
// response contract. The abstain is decided for the same reason as the
|
|
1539
|
+
// transport abstain above: left undecided, the capture's own locator
|
|
1540
|
+
// re-reads the raw call and publishes the carrier.
|
|
1541
|
+
if (carrierRead && 'abstain' in carrierRead) {
|
|
1542
|
+
this.log(`Call result at ${where}: ${carrierRead.abstain}`);
|
|
1543
|
+
return this.transportAbstain(request, callExpr, carrierRead.abstain);
|
|
1544
|
+
}
|
|
1545
|
+
const carrierCandidate = carrierRead?.payload;
|
|
1525
1546
|
// carrick#1841: what the carrier holds goes through the service's wrapper
|
|
1526
1547
|
// rules before it is published, as the call's own result did. The carrier
|
|
1527
1548
|
// is found by its shape, so what it holds can still be a library's
|
|
@@ -1653,7 +1674,7 @@ export class TypeInferrer {
|
|
|
1653
1674
|
const callAfterRules = callUnwrap.wasUnwrapped ? callUnwrap.payloadType : callPayloadType;
|
|
1654
1675
|
const leftCarrier = (callUnwrap.wasUnwrapped || readByProtocol) &&
|
|
1655
1676
|
!!callAfterRules &&
|
|
1656
|
-
this.resultCarrierArguments(callAfterRules, callExpr) !== undefined;
|
|
1677
|
+
this.resultCarrierArguments(callAfterRules, callExpr, callArgs) !== undefined;
|
|
1657
1678
|
const anchorSource = carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
|
|
1658
1679
|
let anchor = anchorSource
|
|
1659
1680
|
? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
|
|
@@ -2461,13 +2482,57 @@ export class TypeInferrer {
|
|
|
2461
2482
|
* Where neither decides, the carrier keeps its own answer and the limit is
|
|
2462
2483
|
* logged: a coin flip published as a contract is worse than an envelope a
|
|
2463
2484
|
* reader can see is an envelope.
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2485
|
+
*
|
|
2486
|
+
* carrick#1841: an argument that is a RESPONSE TABLE (`{ 200: Item }`, every
|
|
2487
|
+
* key a status code) says which side it is by its keys, and that decides
|
|
2488
|
+
* first: the one table whose keys are all 2xx is the success side, and what
|
|
2489
|
+
* a caller receives is the table's body, the member the branch holds, never
|
|
2490
|
+
* the table. A success table whose rows state no body (`{ 204: void }`)
|
|
2491
|
+
* states no response contract. A carrier built from tables, or found
|
|
2492
|
+
* through the call's arguments, that nothing decides states none either:
|
|
2493
|
+
* the union is the client's bookkeeping, not a body, so it is not published
|
|
2494
|
+
* as written.
|
|
2495
|
+
*/
|
|
2496
|
+
resultCarrierPayload(type, at, projections, where, callArgs) {
|
|
2497
|
+
const shape = this.resultCarrierArguments(type, at, callArgs);
|
|
2467
2498
|
if (!shape) {
|
|
2468
2499
|
return undefined;
|
|
2469
2500
|
}
|
|
2470
|
-
const { carrier, carried } = shape;
|
|
2501
|
+
const { carrier, carried, bodies, readOffCall } = shape;
|
|
2502
|
+
const checker = this.project.getTypeChecker().compilerObject;
|
|
2503
|
+
const tables = carried.map((arg) => {
|
|
2504
|
+
const entries = statusTableOf(checker, arg.compilerType);
|
|
2505
|
+
return {
|
|
2506
|
+
arg,
|
|
2507
|
+
entries,
|
|
2508
|
+
side: entries ? classifyStatusCodes(entries.map((entry) => entry.code)) : undefined,
|
|
2509
|
+
};
|
|
2510
|
+
});
|
|
2511
|
+
const successTables = tables.filter((table) => table.side === 'success');
|
|
2512
|
+
if (successTables.length === 1) {
|
|
2513
|
+
const { arg, entries } = successTables[0];
|
|
2514
|
+
const body = bodies.get(arg) ?? this.singleSuccessValue(arg, entries ?? [], at);
|
|
2515
|
+
if (body && statesNoBody(body.compilerType)) {
|
|
2516
|
+
return {
|
|
2517
|
+
abstain: `the call answers a carrier whose success side is the response table ` +
|
|
2518
|
+
`'${typeText(arg, at)}', and its success rows state no body, so this site ` +
|
|
2519
|
+
'states no response contract',
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
if (body) {
|
|
2523
|
+
return { payload: body };
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
const builtFromTables = tables.some((table) => table.side !== undefined);
|
|
2527
|
+
const undecided = (detail) => {
|
|
2528
|
+
this.log(`Call result at ${where} answers a carrier (${typeText(carrier, at)}): ${detail}`);
|
|
2529
|
+
return builtFromTables || readOffCall
|
|
2530
|
+
? {
|
|
2531
|
+
abstain: `the call answers a carrier (${typeText(carrier, at)}) whose success side ` +
|
|
2532
|
+
'cannot be told from its failure side, so this site states no response contract',
|
|
2533
|
+
}
|
|
2534
|
+
: undefined;
|
|
2535
|
+
};
|
|
2471
2536
|
const argTexts = new Map(carried.map((arg) => [arg.getText(), arg]));
|
|
2472
2537
|
const read = new Set();
|
|
2473
2538
|
for (const projection of projections) {
|
|
@@ -2486,27 +2551,69 @@ export class TypeInferrer {
|
|
|
2486
2551
|
// argument out than the shape test picked, the two disagree about which
|
|
2487
2552
|
// side is which and nothing here knows better than the source does.
|
|
2488
2553
|
if (byShape && byRead && byShape !== byRead) {
|
|
2489
|
-
|
|
2490
|
-
`
|
|
2491
|
-
|
|
2492
|
-
|
|
2554
|
+
return undecided(`its error shape names '${typeText(byShape, at)}' as the payload while the source ` +
|
|
2555
|
+
`reads '${typeText(byRead, at)}' out of it. Not picking one`);
|
|
2556
|
+
}
|
|
2557
|
+
const decided = byShape ?? byRead;
|
|
2558
|
+
if (decided) {
|
|
2559
|
+
return { payload: bodies.get(decided) ?? decided };
|
|
2560
|
+
}
|
|
2561
|
+
return undecided('its success side cannot be told from its failure side: no single argument is the ' +
|
|
2562
|
+
'only non-error one and the source reads none of them out. Not guessing which ' +
|
|
2563
|
+
'argument is the payload');
|
|
2564
|
+
}
|
|
2565
|
+
/**
|
|
2566
|
+
* The value of a response table's one success row, for a table a carrier
|
|
2567
|
+
* holds as itself rather than through its body. `undefined` where the table
|
|
2568
|
+
* has more than one success row: their union has no single type here.
|
|
2569
|
+
*/
|
|
2570
|
+
singleSuccessValue(table, entries, at) {
|
|
2571
|
+
const success = successEntries(entries);
|
|
2572
|
+
if (success.length !== 1)
|
|
2493
2573
|
return undefined;
|
|
2574
|
+
return table.getProperty(success[0].key)?.getTypeAtLocation(at);
|
|
2575
|
+
}
|
|
2576
|
+
/**
|
|
2577
|
+
* carrick#1841: the type arguments a call's result was built from, for a
|
|
2578
|
+
* result that no longer carries them. A client whose result type is a
|
|
2579
|
+
* conditional alias (`Result<TData, TError, ThrowOnError>`) resolves to its
|
|
2580
|
+
* branch at the call, and the branch is an anonymous union with no
|
|
2581
|
+
* arguments of its own. The arguments are still stated in two places the
|
|
2582
|
+
* compiler keeps:
|
|
2583
|
+
*
|
|
2584
|
+
* - the call's own explicit type arguments (`client.get<Table, Errors>(…)`);
|
|
2585
|
+
* - the alias the callee's declared return type was written with, where
|
|
2586
|
+
* the callee is a function wrapping such a call
|
|
2587
|
+
* (`getItem<ThrowOnError>(…)` returning `Result<Table, Errors, ThrowOnError>`).
|
|
2588
|
+
*
|
|
2589
|
+
* An argument read here names a payload only where a branch of the result
|
|
2590
|
+
* holds it (`resultCarrierArguments`), so a type parameter of the callee or
|
|
2591
|
+
* a style flag carries nothing.
|
|
2592
|
+
*/
|
|
2593
|
+
callResultArguments(callExpr) {
|
|
2594
|
+
const explicit = callExpr.getTypeArguments().map((node) => node.getType());
|
|
2595
|
+
if (explicit.length > 0)
|
|
2596
|
+
return explicit;
|
|
2597
|
+
try {
|
|
2598
|
+
const declaration = this.project
|
|
2599
|
+
.getTypeChecker()
|
|
2600
|
+
.getResolvedSignature(callExpr)
|
|
2601
|
+
?.getDeclaration();
|
|
2602
|
+
if (declaration && Node.isReturnTyped(declaration)) {
|
|
2603
|
+
return declaration.getReturnType().getAliasTypeArguments();
|
|
2604
|
+
}
|
|
2494
2605
|
}
|
|
2495
|
-
|
|
2496
|
-
|
|
2606
|
+
catch {
|
|
2607
|
+
// A signature the checker cannot resolve states no arguments.
|
|
2497
2608
|
}
|
|
2498
|
-
|
|
2499
|
-
'side cannot be told from its failure side: no single argument is the only ' +
|
|
2500
|
-
'non-error one and the source reads none of them out. Publishing the carrier as ' +
|
|
2501
|
-
'written rather than guessing which argument is the payload');
|
|
2502
|
-
return undefined;
|
|
2609
|
+
return [];
|
|
2503
2610
|
}
|
|
2504
2611
|
/**
|
|
2505
2612
|
* The shape test of `resultCarrierPayload`: the carrier `type` is, once a
|
|
2506
2613
|
* promise-like around it is peeled, and the type arguments a branch of it
|
|
2507
2614
|
* holds as a member. `undefined` when `type` is not a carrier.
|
|
2508
2615
|
*/
|
|
2509
|
-
resultCarrierArguments(type, at) {
|
|
2616
|
+
resultCarrierArguments(type, at, callArgs = []) {
|
|
2510
2617
|
const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
|
|
2511
2618
|
if (!carrier.isUnion()) {
|
|
2512
2619
|
return undefined;
|
|
@@ -2515,26 +2622,80 @@ export class TypeInferrer {
|
|
|
2515
2622
|
if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
|
|
2516
2623
|
return undefined;
|
|
2517
2624
|
}
|
|
2518
|
-
const
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2625
|
+
const own = [...carrier.getAliasTypeArguments(), ...carrier.getTypeArguments()];
|
|
2626
|
+
// carrick#1841: a carrier with no arguments of its own is read through the
|
|
2627
|
+
// arguments its call was built from (`callResultArguments`).
|
|
2628
|
+
const readOffCall = own.length === 0;
|
|
2629
|
+
const args = readOffCall ? callArgs : own;
|
|
2522
2630
|
if (args.length < 2) {
|
|
2523
2631
|
return undefined;
|
|
2524
2632
|
}
|
|
2633
|
+
// carrick#1841: on a carrier read off the call, a member every branch
|
|
2634
|
+
// holds with one type (the request and response objects a client hands
|
|
2635
|
+
// back beside the result) is the transport's, not a side of the result.
|
|
2636
|
+
const transport = readOffCall ? this.membersCommonToEveryBranch(branches, at) : new Set();
|
|
2637
|
+
const checker = this.project.getTypeChecker().compilerObject;
|
|
2638
|
+
// What a branch member holding a response table's body looks like: the
|
|
2639
|
+
// table indexed by its own keys (`Table[keyof Table]`), which the compiler
|
|
2640
|
+
// resolves to the union of the table's values.
|
|
2641
|
+
const tableValues = new Map();
|
|
2642
|
+
for (const arg of args) {
|
|
2643
|
+
const entries = statusTableOf(checker, arg.compilerType);
|
|
2644
|
+
if (entries) {
|
|
2645
|
+
tableValues.set(arg, new Set(entries.flatMap((entry) => constituents(checker.getTypeOfSymbolAtLocation(entry.symbol, at.compilerNode)))));
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
const bodies = new Map();
|
|
2525
2649
|
// A type argument only names a payload when a branch actually holds it:
|
|
2526
2650
|
// a generic that parameterises a status code or a key carries nothing.
|
|
2527
|
-
const carried = args.filter((arg) => branches.some((branch) => branch
|
|
2528
|
-
.
|
|
2529
|
-
|
|
2651
|
+
const carried = args.filter((arg) => branches.some((branch) => branch.getProperties().some((property) => {
|
|
2652
|
+
if (transport.has(property.getName()))
|
|
2653
|
+
return false;
|
|
2530
2654
|
try {
|
|
2531
|
-
|
|
2655
|
+
const held = property.getTypeAtLocation(at);
|
|
2656
|
+
if (held.getText() === arg.getText())
|
|
2657
|
+
return true;
|
|
2658
|
+
const values = tableValues.get(arg);
|
|
2659
|
+
const heldTypes = constituents(held.compilerType);
|
|
2660
|
+
if (values &&
|
|
2661
|
+
heldTypes.length === values.size &&
|
|
2662
|
+
heldTypes.every((member) => values.has(member))) {
|
|
2663
|
+
bodies.set(arg, held);
|
|
2664
|
+
return true;
|
|
2665
|
+
}
|
|
2666
|
+
return false;
|
|
2532
2667
|
}
|
|
2533
2668
|
catch {
|
|
2534
2669
|
return false;
|
|
2535
2670
|
}
|
|
2536
2671
|
})));
|
|
2537
|
-
return carried.length === 0 ? undefined : { carrier, carried };
|
|
2672
|
+
return carried.length === 0 ? undefined : { carrier, carried, bodies, readOffCall };
|
|
2673
|
+
}
|
|
2674
|
+
/** The member names every branch holds, each with one and the same type. */
|
|
2675
|
+
membersCommonToEveryBranch(branches, at) {
|
|
2676
|
+
const [first, ...rest] = branches;
|
|
2677
|
+
const common = new Set();
|
|
2678
|
+
for (const property of first.getProperties()) {
|
|
2679
|
+
const name = property.getName();
|
|
2680
|
+
let held;
|
|
2681
|
+
try {
|
|
2682
|
+
held = property.getTypeAtLocation(at);
|
|
2683
|
+
}
|
|
2684
|
+
catch {
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
const same = rest.every((branch) => {
|
|
2688
|
+
try {
|
|
2689
|
+
return branch.getProperty(name)?.getTypeAtLocation(at).compilerType === held.compilerType;
|
|
2690
|
+
}
|
|
2691
|
+
catch {
|
|
2692
|
+
return false;
|
|
2693
|
+
}
|
|
2694
|
+
});
|
|
2695
|
+
if (same)
|
|
2696
|
+
common.add(name);
|
|
2697
|
+
}
|
|
2698
|
+
return common;
|
|
2538
2699
|
}
|
|
2539
2700
|
/**
|
|
2540
2701
|
* The decided abstain of a call whose result carries transport the
|
|
@@ -2544,16 +2705,16 @@ export class TypeInferrer {
|
|
|
2544
2705
|
* re-reading the raw call (`inference_decided_no_contract`,
|
|
2545
2706
|
* engine/type_compat_v2.rs).
|
|
2546
2707
|
*/
|
|
2547
|
-
transportAbstain(request, callExpr
|
|
2708
|
+
transportAbstain(request, callExpr, detail = "what this call's result carries is transport that the service's wrapper " +
|
|
2709
|
+
'rules verify and read no payload out of (a library response object around the body), ' +
|
|
2710
|
+
'so this site states no response contract') {
|
|
2548
2711
|
const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(callExpr));
|
|
2549
2712
|
abstain.any_provenance = [
|
|
2550
2713
|
{
|
|
2551
2714
|
path: '',
|
|
2552
2715
|
kind: 'unknown',
|
|
2553
2716
|
reason: 'machinery_envelope',
|
|
2554
|
-
detail
|
|
2555
|
-
'verify and read no payload out of (a library response object around the body), ' +
|
|
2556
|
-
'so this site states no response contract',
|
|
2717
|
+
detail,
|
|
2557
2718
|
},
|
|
2558
2719
|
];
|
|
2559
2720
|
return abstain;
|
|
@@ -3913,17 +4074,13 @@ export class TypeInferrer {
|
|
|
3913
4074
|
const isEmptyObject = (text) => text.replace(/\s+/g, '') === '{}';
|
|
3914
4075
|
const withMembers = candidates.filter((c) => !isEmptyObject(c.typeString));
|
|
3915
4076
|
const kept = withMembers.length > 0 ? withMembers : candidates;
|
|
3916
|
-
|
|
3917
|
-
const seen = new Set();
|
|
3918
|
-
const distinct = kept.filter((c) => {
|
|
3919
|
-
const key = c.typeString.replace(/\s+/g, ' ').trim();
|
|
3920
|
-
if (seen.has(key))
|
|
3921
|
-
return false;
|
|
3922
|
-
seen.add(key);
|
|
3923
|
-
return true;
|
|
3924
|
-
});
|
|
4077
|
+
const distinct = distinctByText(kept, (c) => c.typeString);
|
|
3925
4078
|
const typeString = distinct.map((c) => c.typeString).join(' | ');
|
|
3926
4079
|
return {
|
|
4080
|
+
// carrick#2054: which request field picks each member of the union.
|
|
4081
|
+
responseModes: distinct.length >= 2
|
|
4082
|
+
? this.responseModesOf(kept.map((c) => ({ node: c.node, type_string: c.typeString })))
|
|
4083
|
+
: undefined,
|
|
3927
4084
|
typeString,
|
|
3928
4085
|
// Every payload node that fed the union, deduped ones included: a
|
|
3929
4086
|
// caller asks whether the expression it located is one of them.
|
|
@@ -3937,6 +4094,20 @@ export class TypeInferrer {
|
|
|
3937
4094
|
statedTypeNode: distinct.length === 1 ? distinct[0].statedTypeNode : undefined,
|
|
3938
4095
|
};
|
|
3939
4096
|
}
|
|
4097
|
+
/**
|
|
4098
|
+
* The request field each member of a joined union answers
|
|
4099
|
+
* (`response-modes.ts`). Case texts are joined by the published union's
|
|
4100
|
+
* rule. A reading that fails is no reading: it must never cost the union.
|
|
4101
|
+
*/
|
|
4102
|
+
responseModesOf(bodies) {
|
|
4103
|
+
try {
|
|
4104
|
+
return readResponseModes(bodies, (texts) => distinctByText(texts, (text) => text).join(' | '), (symbol) => this.symbolIsLibOrExternalOrigin(symbol));
|
|
4105
|
+
}
|
|
4106
|
+
catch (error) {
|
|
4107
|
+
this.log(`Response modes not read: ${error instanceof Error ? error.message : String(error)}`);
|
|
4108
|
+
return undefined;
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
3940
4111
|
/**
|
|
3941
4112
|
* The expressions a function returns, excluding returns belonging to nested
|
|
3942
4113
|
* functions (a `.map(x => …)` callback returns a row, not the response).
|
|
@@ -4625,7 +4796,7 @@ export class TypeInferrer {
|
|
|
4625
4796
|
*/
|
|
4626
4797
|
declaringPackageOf(type) {
|
|
4627
4798
|
const symbol = type.getSymbol() ?? type.getAliasSymbol();
|
|
4628
|
-
const declaration = symbol?.getDeclarations()
|
|
4799
|
+
const declaration = this.attributedDeclaration(symbol?.getDeclarations() ?? []);
|
|
4629
4800
|
if (!declaration) {
|
|
4630
4801
|
return undefined;
|
|
4631
4802
|
}
|
|
@@ -4648,6 +4819,28 @@ export class TypeInferrer {
|
|
|
4648
4819
|
}
|
|
4649
4820
|
return rest[0];
|
|
4650
4821
|
}
|
|
4822
|
+
/**
|
|
4823
|
+
* The declaration a symbol is attributed to (carrick#2031). One declaration
|
|
4824
|
+
* is itself. Where several files merge one symbol (a global interface the
|
|
4825
|
+
* compiler's library declares and a typings package augments), their order
|
|
4826
|
+
* is the order the checker merged them in, and on TypeScript 6 that changes
|
|
4827
|
+
* once the program is rebuilt, so the first one is not an answer. The
|
|
4828
|
+
* compiler's default library wins where it declares the symbol; otherwise,
|
|
4829
|
+
* and among the library's own files, the declaring file whose path sorts
|
|
4830
|
+
* first, then the earliest declaration in it.
|
|
4831
|
+
*/
|
|
4832
|
+
attributedDeclaration(declarations) {
|
|
4833
|
+
if (declarations.length <= 1)
|
|
4834
|
+
return declarations[0];
|
|
4835
|
+
const program = this.project.getProgram().compilerObject;
|
|
4836
|
+
const fromLibrary = declarations.filter((declaration) => program.isSourceFileDefaultLibrary(declaration.getSourceFile().compilerNode));
|
|
4837
|
+
const candidates = fromLibrary.length > 0 ? fromLibrary : declarations;
|
|
4838
|
+
return [...candidates].sort((a, b) => {
|
|
4839
|
+
const pathA = a.getSourceFile().getFilePath();
|
|
4840
|
+
const pathB = b.getSourceFile().getFilePath();
|
|
4841
|
+
return pathA < pathB ? -1 : pathA > pathB ? 1 : a.getStart() - b.getStart();
|
|
4842
|
+
})[0];
|
|
4843
|
+
}
|
|
4651
4844
|
/**
|
|
4652
4845
|
* The awaited return type of the member invoked on the receiver. Reported as
|
|
4653
4846
|
* a fact next to the receiver; a caller that classified on it alone would be
|
|
@@ -4795,7 +4988,7 @@ export class TypeInferrer {
|
|
|
4795
4988
|
for (const node of sourceFile.getDescendants()) {
|
|
4796
4989
|
if ((Node.isCallExpression(node) ||
|
|
4797
4990
|
Node.isObjectLiteralExpression(node)) &&
|
|
4798
|
-
node
|
|
4991
|
+
startLineOf(node) === line) {
|
|
4799
4992
|
const handler = this.resolveRegisteredHandler(node);
|
|
4800
4993
|
if (handler) {
|
|
4801
4994
|
return { registration: node, handler };
|
|
@@ -5936,14 +6129,9 @@ export class TypeInferrer {
|
|
|
5936
6129
|
if (!Node.isPropertyAssignment(prop)) {
|
|
5937
6130
|
continue;
|
|
5938
6131
|
}
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
? Number(name)
|
|
5943
|
-
: /^\dxx$/.test(name)
|
|
5944
|
-
? Number(name[0]) * 100
|
|
5945
|
-
: NaN;
|
|
5946
|
-
if (!Number.isFinite(code) || code < 200 || code > 299) {
|
|
6132
|
+
// `2xx` is a status RANGE key; it reads as the bottom of its range.
|
|
6133
|
+
const code = statusKeyCode(prop.getName());
|
|
6134
|
+
if (code === undefined || code < 200 || code > 299) {
|
|
5947
6135
|
continue;
|
|
5948
6136
|
}
|
|
5949
6137
|
const initializer = prop.getInitializer();
|
|
@@ -6391,7 +6579,7 @@ export class TypeInferrer {
|
|
|
6391
6579
|
// Filter to nodes within the search window and pre-compute normalized text
|
|
6392
6580
|
const candidates = (lineNumber
|
|
6393
6581
|
? nodes.filter((node) => {
|
|
6394
|
-
const nodeLine = node
|
|
6582
|
+
const nodeLine = startLineOf(node);
|
|
6395
6583
|
return nodeLine >= lineNumber - searchRadius && nodeLine <= lineNumber + searchRadius;
|
|
6396
6584
|
})
|
|
6397
6585
|
: nodes).map((node) => ({ node, text: this.normalizeWhitespace(node.getText()) }));
|
|
@@ -6449,8 +6637,8 @@ export class TypeInferrer {
|
|
|
6449
6637
|
}
|
|
6450
6638
|
// Tie-break by proximity to target line
|
|
6451
6639
|
if (targetLine !== undefined) {
|
|
6452
|
-
const bestDist = Math.abs(best
|
|
6453
|
-
const currentDist = Math.abs(current
|
|
6640
|
+
const bestDist = Math.abs(startLineOf(best) - targetLine);
|
|
6641
|
+
const currentDist = Math.abs(startLineOf(current) - targetLine);
|
|
6454
6642
|
return currentDist < bestDist ? current : best;
|
|
6455
6643
|
}
|
|
6456
6644
|
return best;
|
|
@@ -6666,8 +6854,8 @@ export class TypeInferrer {
|
|
|
6666
6854
|
// Result Building
|
|
6667
6855
|
// ===========================================================================
|
|
6668
6856
|
getNodeLocation(node) {
|
|
6669
|
-
const startLinePos = node
|
|
6670
|
-
const endLinePos = node
|
|
6857
|
+
const startLinePos = startLineOf(node);
|
|
6858
|
+
const endLinePos = endLineOf(node);
|
|
6671
6859
|
const location = {
|
|
6672
6860
|
file_path: node.getSourceFile().getFilePath(),
|
|
6673
6861
|
start_line: startLinePos,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Type definitions for the sidecar message protocol
|
|
3
3
|
* These types define the JSON messages exchanged between Rust and the Node.js sidecar
|
|
4
4
|
*/
|
|
5
|
+
import type { ResponseModes } from './response-modes.js';
|
|
5
6
|
/**
|
|
6
7
|
* The kind of type inference to perform
|
|
7
8
|
*/
|
|
@@ -206,6 +207,22 @@ export interface HealthRequest extends BaseRequest {
|
|
|
206
207
|
export interface ShutdownRequest extends BaseRequest {
|
|
207
208
|
action: 'shutdown';
|
|
208
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* List the root files the init'd project's program was built from, in order
|
|
212
|
+
* (carrick#2027).
|
|
213
|
+
*/
|
|
214
|
+
export interface ListProgramFilesRequest extends BaseRequest {
|
|
215
|
+
action: 'list_program_files';
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Add files to the programs that type them, in the given order
|
|
219
|
+
* (carrick#2027).
|
|
220
|
+
*/
|
|
221
|
+
export interface AddProgramFilesRequest extends BaseRequest {
|
|
222
|
+
action: 'add_program_files';
|
|
223
|
+
/** Absolute paths, or relative to the init'd root. */
|
|
224
|
+
files: string[];
|
|
225
|
+
}
|
|
209
226
|
/**
|
|
210
227
|
* Request to resolve type definitions from a v2 capture stub package.
|
|
211
228
|
* Returns both the original declaration and the compiler-expanded form for
|
|
@@ -461,7 +478,7 @@ export interface ListLibrarySurfaceRequest extends BaseRequest {
|
|
|
461
478
|
/**
|
|
462
479
|
* Union type for all possible sidecar requests
|
|
463
480
|
*/
|
|
464
|
-
export type SidecarRequest = RetypeCheckRequest | VerifyClientSemanticsRequest | VerifyLibraryClaimsRequest | ListLibrarySurfaceRequest | InitRequest | BundleRequest | CaptureV2Request | CheckV2Request | InferRequest | ResolveDefinitionsRequest | HealthRequest | ShutdownRequest;
|
|
481
|
+
export type SidecarRequest = RetypeCheckRequest | VerifyClientSemanticsRequest | VerifyLibraryClaimsRequest | ListLibrarySurfaceRequest | InitRequest | BundleRequest | CaptureV2Request | CheckV2Request | InferRequest | ResolveDefinitionsRequest | HealthRequest | ShutdownRequest | ListProgramFilesRequest | AddProgramFilesRequest;
|
|
465
482
|
/**
|
|
466
483
|
* Request for a specific symbol to be bundled
|
|
467
484
|
*/
|
|
@@ -480,6 +497,12 @@ export interface SymbolRequest {
|
|
|
480
497
|
* symbol as-is (the HTTP/socket/consumer default).
|
|
481
498
|
*/
|
|
482
499
|
array_depth?: number;
|
|
500
|
+
/**
|
|
501
|
+
* carrick#1841: the symbol names what a consumer receives. A symbol that is
|
|
502
|
+
* a response table keyed by status code (`{ 200: Item }`) then bundles as
|
|
503
|
+
* its 2xx body, never as the table.
|
|
504
|
+
*/
|
|
505
|
+
consumer_response?: boolean;
|
|
483
506
|
}
|
|
484
507
|
/**
|
|
485
508
|
* Request for type inference at a specific location
|
|
@@ -645,6 +668,22 @@ export interface HealthResponse extends BaseResponse {
|
|
|
645
668
|
export interface ShutdownResponse extends BaseResponse {
|
|
646
669
|
status: 'success';
|
|
647
670
|
}
|
|
671
|
+
/**
|
|
672
|
+
* Response for list_program_files action
|
|
673
|
+
*/
|
|
674
|
+
export interface ListProgramFilesResponse extends BaseResponse {
|
|
675
|
+
/** The program's root files, as absolute paths, in order. */
|
|
676
|
+
files?: string[];
|
|
677
|
+
errors?: string[];
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Response for add_program_files action
|
|
681
|
+
*/
|
|
682
|
+
export interface AddProgramFilesResponse extends BaseResponse {
|
|
683
|
+
/** How many of the given files were added. */
|
|
684
|
+
added?: number;
|
|
685
|
+
errors?: string[];
|
|
686
|
+
}
|
|
648
687
|
/**
|
|
649
688
|
* Error response
|
|
650
689
|
*/
|
|
@@ -785,7 +824,7 @@ export interface ListLibrarySurfaceResponse extends BaseResponse {
|
|
|
785
824
|
/**
|
|
786
825
|
* Union type for all possible sidecar responses
|
|
787
826
|
*/
|
|
788
|
-
export type SidecarResponse = RetypeCheckResponse | VerifyClientSemanticsResponse | VerifyLibraryClaimsResponse | ListLibrarySurfaceResponse | InitResponse | BundleResponse | CaptureV2Response | CheckV2Response | InferResponse | ResolveDefinitionsResponse | HealthResponse | ShutdownResponse | ErrorResponse;
|
|
827
|
+
export type SidecarResponse = RetypeCheckResponse | VerifyClientSemanticsResponse | VerifyLibraryClaimsResponse | ListLibrarySurfaceResponse | InitResponse | BundleResponse | CaptureV2Response | CheckV2Response | InferResponse | ResolveDefinitionsResponse | HealthResponse | ShutdownResponse | ListProgramFilesResponse | AddProgramFilesResponse | ErrorResponse;
|
|
789
828
|
/**
|
|
790
829
|
* An entry in the type manifest
|
|
791
830
|
*/
|
|
@@ -915,6 +954,15 @@ export interface InferredType {
|
|
|
915
954
|
* Absent when the body is not a raw-text read.
|
|
916
955
|
*/
|
|
917
956
|
raw_text_read?: true;
|
|
957
|
+
/**
|
|
958
|
+
* carrick#2054, a response joined from two or more success bodies: the
|
|
959
|
+
* request fields whose values pick among them (`reads`), and, for one
|
|
960
|
+
* query parameter or body field whose every test the reading follows, the
|
|
961
|
+
* union each stated value receives (`cases`). `type_string` is unchanged.
|
|
962
|
+
* Absent when no request read separates the bodies, or when every value
|
|
963
|
+
* receives the same union.
|
|
964
|
+
*/
|
|
965
|
+
response_modes?: ResponseModes;
|
|
918
966
|
}
|
|
919
967
|
/**
|
|
920
968
|
* What the source states a body read to be (carrick#1749). The scanner
|