carrick 0.3.109 → 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 +6 -6
- 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-workspace.d.ts +10 -0
- package/sidecar/dist/src/capture/check-workspace.js +68 -6
- package/sidecar/dist/src/capture/index.d.ts +1 -0
- package/sidecar/dist/src/capture/index.js +18 -3
- 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/resolution-edges.d.ts +36 -0
- package/sidecar/dist/src/capture/resolution-edges.js +150 -0
- 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/failure-path.d.ts +42 -2
- package/sidecar/dist/src/failure-path.js +38 -13
- 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/response-modes.d.ts +77 -0
- package/sidecar/dist/src/response-modes.js +459 -0
- package/sidecar/dist/src/type-inferrer.d.ts +42 -0
- package/sidecar/dist/src/type-inferrer.js +237 -76
- package/sidecar/dist/src/types.d.ts +16 -0
- package/sidecar/dist/src/validators.d.ts +39 -16
- package/sidecar/dist/src/validators.js +4 -0
|
@@ -24,8 +24,11 @@ import { validateInferRequestItem } from './validators.js';
|
|
|
24
24
|
import { notePrintedType, PrintedTypes } from './printed-names.js';
|
|
25
25
|
import { externalImportsOf, isExternalOrigin } from './origin.js';
|
|
26
26
|
import { reachedOnlyOnFailure } from './failure-path.js';
|
|
27
|
+
import { readResponseModes } from './response-modes.js';
|
|
27
28
|
import { functionAtLine } from './function-line-index.js';
|
|
29
|
+
import { endLineOf, startLineOf } from './line-index.js';
|
|
28
30
|
import { elapsedMs, inferTiming, phaseClock, timedPhase } from './infer-timing.js';
|
|
31
|
+
import { classifyStatusCodes, constituents, httpStatus, statesNoBody, statusKeyCode, statusTableOf, successEntries, } from './capture/index.js';
|
|
29
32
|
import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
|
|
30
33
|
import { expandTypeStructural, jsonWireType, } from './type-structural-expander.js';
|
|
31
34
|
/**
|
|
@@ -169,11 +172,19 @@ const RESPONSE_HELPER_MAX_DEPTH = 4;
|
|
|
169
172
|
* the branch an error path, whose shape is not the endpoint's contract.
|
|
170
173
|
*/
|
|
171
174
|
const STATUS_MEMBER_NAMES = ['status', 'statusCode'];
|
|
172
|
-
/**
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
});
|
|
177
188
|
}
|
|
178
189
|
/**
|
|
179
190
|
* The status codes a TYPE fixes: a numeric literal, or a union made only of
|
|
@@ -193,16 +204,6 @@ function statusCodesOfType(type) {
|
|
|
193
204
|
}
|
|
194
205
|
return codes.length > 0 ? codes : undefined;
|
|
195
206
|
}
|
|
196
|
-
/** One verdict for a set of status codes, or `mixed` when they disagree. */
|
|
197
|
-
function classifyStatusCodes(codes) {
|
|
198
|
-
if (codes.every((code) => code >= 400))
|
|
199
|
-
return 'error';
|
|
200
|
-
if (codes.every((code) => code >= 300 && code < 400))
|
|
201
|
-
return 'redirect';
|
|
202
|
-
if (codes.every((code) => code < 300))
|
|
203
|
-
return 'success';
|
|
204
|
-
return 'mixed';
|
|
205
|
-
}
|
|
206
207
|
/**
|
|
207
208
|
* The parts of a request that ARE the body, as validator middleware names them
|
|
208
209
|
* (`validate('json', Schema)`). HTTP vocabulary for how a body is carried — the
|
|
@@ -973,7 +974,7 @@ export class TypeInferrer {
|
|
|
973
974
|
Node.isArrowFunction(ancestor) ||
|
|
974
975
|
Node.isFunctionExpression(ancestor) ||
|
|
975
976
|
Node.isMethodDeclaration(ancestor)) &&
|
|
976
|
-
ancestor
|
|
977
|
+
startLineOf(ancestor) === request.line_number);
|
|
977
978
|
const declaredHandler = insideFunctionOnLine
|
|
978
979
|
? this.handlerDeclaredAtLine(sourceFile, request.line_number)
|
|
979
980
|
: undefined;
|
|
@@ -1130,7 +1131,7 @@ export class TypeInferrer {
|
|
|
1130
1131
|
*/
|
|
1131
1132
|
exportedFunctionOpeningOn(sourceFile, line) {
|
|
1132
1133
|
const declared = this.findFunctionByLine(sourceFile, line);
|
|
1133
|
-
if (!declared || declared
|
|
1134
|
+
if (!declared || startLineOf(declared) !== line)
|
|
1134
1135
|
return undefined;
|
|
1135
1136
|
if (Node.isFunctionDeclaration(declared)) {
|
|
1136
1137
|
return Node.isSourceFile(declared.getParent()) && declared.isExported()
|
|
@@ -1299,7 +1300,10 @@ export class TypeInferrer {
|
|
|
1299
1300
|
const resolvedSource = resolvedSymbol !== undefined && recoveredAnchor
|
|
1300
1301
|
? this.primaryTypeSymbolSource(recoveredAnchor.element)
|
|
1301
1302
|
: undefined;
|
|
1302
|
-
|
|
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;
|
|
1303
1307
|
}
|
|
1304
1308
|
/**
|
|
1305
1309
|
* carrick#1161: type a route response from every send in the handler the
|
|
@@ -1472,7 +1476,9 @@ export class TypeInferrer {
|
|
|
1472
1476
|
// carrier read gives its own. A result that is no carrier keeps the
|
|
1473
1477
|
// answer it had.
|
|
1474
1478
|
const where = `${request.file_path}:${request.line_number}`;
|
|
1475
|
-
const
|
|
1479
|
+
const callArgs = this.callResultArguments(callExpr);
|
|
1480
|
+
const resultIsCarrier = !explicitType &&
|
|
1481
|
+
this.resultCarrierArguments(returnType, terminalNode, callArgs) !== undefined;
|
|
1476
1482
|
// carrick#1877: the terminal holds a thenable the names do not peel, a
|
|
1477
1483
|
// subclass of `Promise` or a class with a `then` of its own. What a
|
|
1478
1484
|
// caller receives is what `await` yields for it, as it is for the
|
|
@@ -1521,12 +1527,22 @@ export class TypeInferrer {
|
|
|
1521
1527
|
const readThroughCarrier = unwrapResult.wasUnwrapped &&
|
|
1522
1528
|
resultIsCarrier &&
|
|
1523
1529
|
!!afterRules &&
|
|
1524
|
-
this.resultCarrierArguments(afterRules, terminalNode) === undefined;
|
|
1525
|
-
const
|
|
1530
|
+
this.resultCarrierArguments(afterRules, terminalNode, callArgs) === undefined;
|
|
1531
|
+
const carrierRead = explicitType || !afterRules
|
|
1526
1532
|
? undefined
|
|
1527
1533
|
: readThroughCarrier
|
|
1528
|
-
? afterRules
|
|
1529
|
-
: 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;
|
|
1530
1546
|
// carrick#1841: what the carrier holds goes through the service's wrapper
|
|
1531
1547
|
// rules before it is published, as the call's own result did. The carrier
|
|
1532
1548
|
// is found by its shape, so what it holds can still be a library's
|
|
@@ -1658,7 +1674,7 @@ export class TypeInferrer {
|
|
|
1658
1674
|
const callAfterRules = callUnwrap.wasUnwrapped ? callUnwrap.payloadType : callPayloadType;
|
|
1659
1675
|
const leftCarrier = (callUnwrap.wasUnwrapped || readByProtocol) &&
|
|
1660
1676
|
!!callAfterRules &&
|
|
1661
|
-
this.resultCarrierArguments(callAfterRules, callExpr) !== undefined;
|
|
1677
|
+
this.resultCarrierArguments(callAfterRules, callExpr, callArgs) !== undefined;
|
|
1662
1678
|
const anchorSource = carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
|
|
1663
1679
|
let anchor = anchorSource
|
|
1664
1680
|
? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
|
|
@@ -2466,13 +2482,57 @@ export class TypeInferrer {
|
|
|
2466
2482
|
* Where neither decides, the carrier keeps its own answer and the limit is
|
|
2467
2483
|
* logged: a coin flip published as a contract is worse than an envelope a
|
|
2468
2484
|
* reader can see is an envelope.
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
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);
|
|
2472
2498
|
if (!shape) {
|
|
2473
2499
|
return undefined;
|
|
2474
2500
|
}
|
|
2475
|
-
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
|
+
};
|
|
2476
2536
|
const argTexts = new Map(carried.map((arg) => [arg.getText(), arg]));
|
|
2477
2537
|
const read = new Set();
|
|
2478
2538
|
for (const projection of projections) {
|
|
@@ -2491,27 +2551,69 @@ export class TypeInferrer {
|
|
|
2491
2551
|
// argument out than the shape test picked, the two disagree about which
|
|
2492
2552
|
// side is which and nothing here knows better than the source does.
|
|
2493
2553
|
if (byShape && byRead && byShape !== byRead) {
|
|
2494
|
-
|
|
2495
|
-
`
|
|
2496
|
-
|
|
2497
|
-
|
|
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)
|
|
2498
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
|
+
}
|
|
2499
2605
|
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2606
|
+
catch {
|
|
2607
|
+
// A signature the checker cannot resolve states no arguments.
|
|
2502
2608
|
}
|
|
2503
|
-
|
|
2504
|
-
'side cannot be told from its failure side: no single argument is the only ' +
|
|
2505
|
-
'non-error one and the source reads none of them out. Publishing the carrier as ' +
|
|
2506
|
-
'written rather than guessing which argument is the payload');
|
|
2507
|
-
return undefined;
|
|
2609
|
+
return [];
|
|
2508
2610
|
}
|
|
2509
2611
|
/**
|
|
2510
2612
|
* The shape test of `resultCarrierPayload`: the carrier `type` is, once a
|
|
2511
2613
|
* promise-like around it is peeled, and the type arguments a branch of it
|
|
2512
2614
|
* holds as a member. `undefined` when `type` is not a carrier.
|
|
2513
2615
|
*/
|
|
2514
|
-
resultCarrierArguments(type, at) {
|
|
2616
|
+
resultCarrierArguments(type, at, callArgs = []) {
|
|
2515
2617
|
const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
|
|
2516
2618
|
if (!carrier.isUnion()) {
|
|
2517
2619
|
return undefined;
|
|
@@ -2520,26 +2622,80 @@ export class TypeInferrer {
|
|
|
2520
2622
|
if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
|
|
2521
2623
|
return undefined;
|
|
2522
2624
|
}
|
|
2523
|
-
const
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
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;
|
|
2527
2630
|
if (args.length < 2) {
|
|
2528
2631
|
return undefined;
|
|
2529
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();
|
|
2530
2649
|
// A type argument only names a payload when a branch actually holds it:
|
|
2531
2650
|
// a generic that parameterises a status code or a key carries nothing.
|
|
2532
|
-
const carried = args.filter((arg) => branches.some((branch) => branch
|
|
2533
|
-
.
|
|
2534
|
-
|
|
2651
|
+
const carried = args.filter((arg) => branches.some((branch) => branch.getProperties().some((property) => {
|
|
2652
|
+
if (transport.has(property.getName()))
|
|
2653
|
+
return false;
|
|
2535
2654
|
try {
|
|
2536
|
-
|
|
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;
|
|
2537
2667
|
}
|
|
2538
2668
|
catch {
|
|
2539
2669
|
return false;
|
|
2540
2670
|
}
|
|
2541
2671
|
})));
|
|
2542
|
-
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;
|
|
2543
2699
|
}
|
|
2544
2700
|
/**
|
|
2545
2701
|
* The decided abstain of a call whose result carries transport the
|
|
@@ -2549,16 +2705,16 @@ export class TypeInferrer {
|
|
|
2549
2705
|
* re-reading the raw call (`inference_decided_no_contract`,
|
|
2550
2706
|
* engine/type_compat_v2.rs).
|
|
2551
2707
|
*/
|
|
2552
|
-
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') {
|
|
2553
2711
|
const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(callExpr));
|
|
2554
2712
|
abstain.any_provenance = [
|
|
2555
2713
|
{
|
|
2556
2714
|
path: '',
|
|
2557
2715
|
kind: 'unknown',
|
|
2558
2716
|
reason: 'machinery_envelope',
|
|
2559
|
-
detail
|
|
2560
|
-
'verify and read no payload out of (a library response object around the body), ' +
|
|
2561
|
-
'so this site states no response contract',
|
|
2717
|
+
detail,
|
|
2562
2718
|
},
|
|
2563
2719
|
];
|
|
2564
2720
|
return abstain;
|
|
@@ -3918,17 +4074,13 @@ export class TypeInferrer {
|
|
|
3918
4074
|
const isEmptyObject = (text) => text.replace(/\s+/g, '') === '{}';
|
|
3919
4075
|
const withMembers = candidates.filter((c) => !isEmptyObject(c.typeString));
|
|
3920
4076
|
const kept = withMembers.length > 0 ? withMembers : candidates;
|
|
3921
|
-
|
|
3922
|
-
const seen = new Set();
|
|
3923
|
-
const distinct = kept.filter((c) => {
|
|
3924
|
-
const key = c.typeString.replace(/\s+/g, ' ').trim();
|
|
3925
|
-
if (seen.has(key))
|
|
3926
|
-
return false;
|
|
3927
|
-
seen.add(key);
|
|
3928
|
-
return true;
|
|
3929
|
-
});
|
|
4077
|
+
const distinct = distinctByText(kept, (c) => c.typeString);
|
|
3930
4078
|
const typeString = distinct.map((c) => c.typeString).join(' | ');
|
|
3931
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,
|
|
3932
4084
|
typeString,
|
|
3933
4085
|
// Every payload node that fed the union, deduped ones included: a
|
|
3934
4086
|
// caller asks whether the expression it located is one of them.
|
|
@@ -3942,6 +4094,20 @@ export class TypeInferrer {
|
|
|
3942
4094
|
statedTypeNode: distinct.length === 1 ? distinct[0].statedTypeNode : undefined,
|
|
3943
4095
|
};
|
|
3944
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
|
+
}
|
|
3945
4111
|
/**
|
|
3946
4112
|
* The expressions a function returns, excluding returns belonging to nested
|
|
3947
4113
|
* functions (a `.map(x => …)` callback returns a row, not the response).
|
|
@@ -4822,7 +4988,7 @@ export class TypeInferrer {
|
|
|
4822
4988
|
for (const node of sourceFile.getDescendants()) {
|
|
4823
4989
|
if ((Node.isCallExpression(node) ||
|
|
4824
4990
|
Node.isObjectLiteralExpression(node)) &&
|
|
4825
|
-
node
|
|
4991
|
+
startLineOf(node) === line) {
|
|
4826
4992
|
const handler = this.resolveRegisteredHandler(node);
|
|
4827
4993
|
if (handler) {
|
|
4828
4994
|
return { registration: node, handler };
|
|
@@ -5963,14 +6129,9 @@ export class TypeInferrer {
|
|
|
5963
6129
|
if (!Node.isPropertyAssignment(prop)) {
|
|
5964
6130
|
continue;
|
|
5965
6131
|
}
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
? Number(name)
|
|
5970
|
-
: /^\dxx$/.test(name)
|
|
5971
|
-
? Number(name[0]) * 100
|
|
5972
|
-
: NaN;
|
|
5973
|
-
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) {
|
|
5974
6135
|
continue;
|
|
5975
6136
|
}
|
|
5976
6137
|
const initializer = prop.getInitializer();
|
|
@@ -6418,7 +6579,7 @@ export class TypeInferrer {
|
|
|
6418
6579
|
// Filter to nodes within the search window and pre-compute normalized text
|
|
6419
6580
|
const candidates = (lineNumber
|
|
6420
6581
|
? nodes.filter((node) => {
|
|
6421
|
-
const nodeLine = node
|
|
6582
|
+
const nodeLine = startLineOf(node);
|
|
6422
6583
|
return nodeLine >= lineNumber - searchRadius && nodeLine <= lineNumber + searchRadius;
|
|
6423
6584
|
})
|
|
6424
6585
|
: nodes).map((node) => ({ node, text: this.normalizeWhitespace(node.getText()) }));
|
|
@@ -6476,8 +6637,8 @@ export class TypeInferrer {
|
|
|
6476
6637
|
}
|
|
6477
6638
|
// Tie-break by proximity to target line
|
|
6478
6639
|
if (targetLine !== undefined) {
|
|
6479
|
-
const bestDist = Math.abs(best
|
|
6480
|
-
const currentDist = Math.abs(current
|
|
6640
|
+
const bestDist = Math.abs(startLineOf(best) - targetLine);
|
|
6641
|
+
const currentDist = Math.abs(startLineOf(current) - targetLine);
|
|
6481
6642
|
return currentDist < bestDist ? current : best;
|
|
6482
6643
|
}
|
|
6483
6644
|
return best;
|
|
@@ -6693,8 +6854,8 @@ export class TypeInferrer {
|
|
|
6693
6854
|
// Result Building
|
|
6694
6855
|
// ===========================================================================
|
|
6695
6856
|
getNodeLocation(node) {
|
|
6696
|
-
const startLinePos = node
|
|
6697
|
-
const endLinePos = node
|
|
6857
|
+
const startLinePos = startLineOf(node);
|
|
6858
|
+
const endLinePos = endLineOf(node);
|
|
6698
6859
|
const location = {
|
|
6699
6860
|
file_path: node.getSourceFile().getFilePath(),
|
|
6700
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
|
*/
|
|
@@ -496,6 +497,12 @@ export interface SymbolRequest {
|
|
|
496
497
|
* symbol as-is (the HTTP/socket/consumer default).
|
|
497
498
|
*/
|
|
498
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;
|
|
499
506
|
}
|
|
500
507
|
/**
|
|
501
508
|
* Request for type inference at a specific location
|
|
@@ -947,6 +954,15 @@ export interface InferredType {
|
|
|
947
954
|
* Absent when the body is not a raw-text read.
|
|
948
955
|
*/
|
|
949
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;
|
|
950
966
|
}
|
|
951
967
|
/**
|
|
952
968
|
* What the source states a body read to be (carrick#1749). The scanner
|