carrick 0.3.109 → 0.3.111
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/dist/render.d.ts +14 -7
- package/dist/render.js +49 -36
- package/dist/render.js.map +1 -1
- 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 +39 -0
- package/sidecar/dist/src/capture/anchors.js +35 -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 +93 -1
- package/sidecar/dist/src/type-inferrer.js +436 -97
- package/sidecar/dist/src/types.d.ts +20 -1
- 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;
|
|
@@ -1047,7 +1048,11 @@ export class TypeInferrer {
|
|
|
1047
1048
|
// argument is the route path, not a payload. The span locator falls
|
|
1048
1049
|
// back to exactly this shape when no payload expression was reported,
|
|
1049
1050
|
// so drilling here would put the path literal's type in the manifest.
|
|
1050
|
-
|
|
1051
|
+
// A call handed a function whose result is plain data is not a
|
|
1052
|
+
// registration but the payload: `rows.map((r) => ({ ... }))` sends the
|
|
1053
|
+
// list, not the callback's return (carrick#2055).
|
|
1054
|
+
const registersCallback = args.some((arg) => Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) &&
|
|
1055
|
+
!this.callResultIsPlainData(node);
|
|
1051
1056
|
if (registersCallback) {
|
|
1052
1057
|
// A registration that DECLARES its response contract in a schema needs
|
|
1053
1058
|
// no indirection: the declaration is the contract the framework
|
|
@@ -1098,7 +1103,9 @@ export class TypeInferrer {
|
|
|
1098
1103
|
// `typeText` keeps the bare name `Payment`, which dangles in the
|
|
1099
1104
|
// source-less cross-repo bundle → `any` → unverifiable. Expand the
|
|
1100
1105
|
// resolved object structurally so the real members land in the bundle.
|
|
1101
|
-
|
|
1106
|
+
// The name it falls back to is the awaited one: a located call under
|
|
1107
|
+
// `await` sends what it resolves to, never the promise (carrick#2055).
|
|
1108
|
+
typeString = this.expandResolvedTypeStructural(resolved, typeText(resolved, payloadNode), this.wireFormatFor(request));
|
|
1102
1109
|
}
|
|
1103
1110
|
const anchor = this.unwrapArrayLevels(this.unwrapPromiseType(payloadType));
|
|
1104
1111
|
return this.createInferredType(request, typeString, false, this.getNodeLocation(payloadNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth, this.primaryTypeSymbolSource(anchor.element));
|
|
@@ -1130,7 +1137,7 @@ export class TypeInferrer {
|
|
|
1130
1137
|
*/
|
|
1131
1138
|
exportedFunctionOpeningOn(sourceFile, line) {
|
|
1132
1139
|
const declared = this.findFunctionByLine(sourceFile, line);
|
|
1133
|
-
if (!declared || declared
|
|
1140
|
+
if (!declared || startLineOf(declared) !== line)
|
|
1134
1141
|
return undefined;
|
|
1135
1142
|
if (Node.isFunctionDeclaration(declared)) {
|
|
1136
1143
|
return Node.isSourceFile(declared.getParent()) && declared.isExported()
|
|
@@ -1256,6 +1263,8 @@ export class TypeInferrer {
|
|
|
1256
1263
|
* returns the repo's own type (`toInstance(View, plain)`) is the payload.
|
|
1257
1264
|
*/
|
|
1258
1265
|
callResultIsPayload(call) {
|
|
1266
|
+
if (this.callResultIsPlainData(call))
|
|
1267
|
+
return true;
|
|
1259
1268
|
const { element } = this.unwrapArrayLevels(this.unwrapPromiseType(call.getType()));
|
|
1260
1269
|
if (this.symbolIsLibOrExternalOrigin(element.getSymbol() ?? element.getAliasSymbol())) {
|
|
1261
1270
|
return false;
|
|
@@ -1265,6 +1274,58 @@ export class TypeInferrer {
|
|
|
1265
1274
|
// the call that built it would publish what it was built from.
|
|
1266
1275
|
return this.nodeCarriesPayloadContract(call, false, false);
|
|
1267
1276
|
}
|
|
1277
|
+
/**
|
|
1278
|
+
* True when a located call evaluates to plain data, so it is the payload
|
|
1279
|
+
* whatever its arguments and wherever its type is declared (carrick#2055).
|
|
1280
|
+
*
|
|
1281
|
+
* `rows.map(toDto)`, `rows.filter(cb)` and `listItems(ownerId)` produce the
|
|
1282
|
+
* list a route sends. Read as a send, the first produced the mapper's
|
|
1283
|
+
* function type, the second a callback's return, and the third `string`.
|
|
1284
|
+
* What a send or a registration hands back is never plain data: a reply
|
|
1285
|
+
* builder or a router carries methods, a bare send returns `void`, and a
|
|
1286
|
+
* serialiser returns a primitive. So the result alone tells them apart.
|
|
1287
|
+
*
|
|
1288
|
+
* Plain data, once promise levels are taken off: an object with at least
|
|
1289
|
+
* one member, no call or construct signature, and no member that is itself
|
|
1290
|
+
* callable; an array of one; or a union whose members other than `null` and
|
|
1291
|
+
* `undefined` all are. Tuples and empty objects are left to the callers'
|
|
1292
|
+
* own reading.
|
|
1293
|
+
*/
|
|
1294
|
+
callResultIsPlainData(call) {
|
|
1295
|
+
const isPlainData = (type, depth) => {
|
|
1296
|
+
if (depth > 3)
|
|
1297
|
+
return false;
|
|
1298
|
+
if (type.isAny() || type.isUnknown() || type.isNever())
|
|
1299
|
+
return false;
|
|
1300
|
+
if (type.isUnion()) {
|
|
1301
|
+
const members = type
|
|
1302
|
+
.getUnionTypes()
|
|
1303
|
+
.filter((member) => !member.isNull() && !member.isUndefined());
|
|
1304
|
+
return members.length > 0 && members.every((member) => isPlainData(member, depth + 1));
|
|
1305
|
+
}
|
|
1306
|
+
if (type.isIntersection()) {
|
|
1307
|
+
return type.getIntersectionTypes().every((member) => isPlainData(member, depth + 1));
|
|
1308
|
+
}
|
|
1309
|
+
const element = type.getArrayElementType();
|
|
1310
|
+
if (element)
|
|
1311
|
+
return isPlainData(element, depth + 1);
|
|
1312
|
+
if (!type.isObject() || type.isTuple() || this.isCallableType(type))
|
|
1313
|
+
return false;
|
|
1314
|
+
if (this.typeIsOrContainsResponseMachinery(type))
|
|
1315
|
+
return false;
|
|
1316
|
+
const members = type.getProperties();
|
|
1317
|
+
const indexed = type.getStringIndexType() ?? type.getNumberIndexType();
|
|
1318
|
+
if (members.length === 0 && !indexed)
|
|
1319
|
+
return false;
|
|
1320
|
+
const callable = (held) => held.isUnion()
|
|
1321
|
+
? held.getUnionTypes().some((member) => this.isCallableType(member))
|
|
1322
|
+
: this.isCallableType(held);
|
|
1323
|
+
if (indexed && callable(indexed))
|
|
1324
|
+
return false;
|
|
1325
|
+
return members.every((member) => !callable(member.getTypeAtLocation(call)));
|
|
1326
|
+
};
|
|
1327
|
+
return isPlainData(this.unwrapPromiseType(call.getType()), 0);
|
|
1328
|
+
}
|
|
1268
1329
|
/**
|
|
1269
1330
|
* The wire representation a request's printed type takes. A route response
|
|
1270
1331
|
* is serialised as JSON by every sender this layer reads a payload out of,
|
|
@@ -1299,7 +1360,10 @@ export class TypeInferrer {
|
|
|
1299
1360
|
const resolvedSource = resolvedSymbol !== undefined && recoveredAnchor
|
|
1300
1361
|
? this.primaryTypeSymbolSource(recoveredAnchor.element)
|
|
1301
1362
|
: undefined;
|
|
1302
|
-
|
|
1363
|
+
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);
|
|
1364
|
+
if (recovered.responseModes)
|
|
1365
|
+
inferred.response_modes = recovered.responseModes;
|
|
1366
|
+
return inferred;
|
|
1303
1367
|
}
|
|
1304
1368
|
/**
|
|
1305
1369
|
* carrick#1161: type a route response from every send in the handler the
|
|
@@ -1316,7 +1380,8 @@ export class TypeInferrer {
|
|
|
1316
1380
|
*
|
|
1317
1381
|
* Returns `undefined` when this does not apply, which leaves the caller's
|
|
1318
1382
|
* own reading of the located expression untouched:
|
|
1319
|
-
* - the row's line is
|
|
1383
|
+
* - the row's line is neither a route registration nor a line inside the
|
|
1384
|
+
* handler the located send sits in;
|
|
1320
1385
|
* - the located expression is neither a send nor the argument of one;
|
|
1321
1386
|
* - that send is not one the handler returns;
|
|
1322
1387
|
* - it is a success send and the only one that survives, so the join would
|
|
@@ -1329,11 +1394,18 @@ export class TypeInferrer {
|
|
|
1329
1394
|
* or the redirect location back in.
|
|
1330
1395
|
*/
|
|
1331
1396
|
inferFromResponseSites(sourceFile, request, located, extractionConfig) {
|
|
1332
|
-
if (!this.registrationAtLine(sourceFile, request.line_number))
|
|
1333
|
-
return undefined;
|
|
1334
1397
|
const func = this.findContainingFunctionForNode(located);
|
|
1335
1398
|
if (!func)
|
|
1336
1399
|
return undefined;
|
|
1400
|
+
// The row's line is a registration, or a line inside the handler the
|
|
1401
|
+
// located send sits in (carrick#2005: a file-based route's row names the
|
|
1402
|
+
// send's own line, and a send's line is no registration). A row whose
|
|
1403
|
+
// line is outside that function says nothing about it.
|
|
1404
|
+
if (!this.registrationAtLine(sourceFile, request.line_number) &&
|
|
1405
|
+
!(func.getStartLineNumber() <= request.line_number &&
|
|
1406
|
+
request.line_number <= func.getEndLineNumber())) {
|
|
1407
|
+
return undefined;
|
|
1408
|
+
}
|
|
1337
1409
|
const branches = this.responseReturnedExpressions(func).flatMap((expression) => this.expandResponseBranches(expression, 0));
|
|
1338
1410
|
if (branches.length === 0)
|
|
1339
1411
|
return undefined;
|
|
@@ -1348,10 +1420,10 @@ export class TypeInferrer {
|
|
|
1348
1420
|
if (!locatedBranch)
|
|
1349
1421
|
return undefined;
|
|
1350
1422
|
const dropped = (status) => status === 'error' || status === 'redirect';
|
|
1351
|
-
const siteStatus = this.responseSiteStatus(site);
|
|
1423
|
+
const siteStatus = this.responseSiteStatus(site, true);
|
|
1352
1424
|
const survivors = [];
|
|
1353
1425
|
for (const branch of branches) {
|
|
1354
|
-
const status = branch === locatedBranch ? siteStatus : this.responseSiteStatus(branch);
|
|
1426
|
+
const status = branch === locatedBranch ? siteStatus : this.responseSiteStatus(branch, true);
|
|
1355
1427
|
if (status === 'variable') {
|
|
1356
1428
|
this.log(`Response status at ${request.file_path}:${this.getNodeLocation(branch).start_line} ` +
|
|
1357
1429
|
'is not a literal; keeping that send in the route contract');
|
|
@@ -1472,7 +1544,9 @@ export class TypeInferrer {
|
|
|
1472
1544
|
// carrier read gives its own. A result that is no carrier keeps the
|
|
1473
1545
|
// answer it had.
|
|
1474
1546
|
const where = `${request.file_path}:${request.line_number}`;
|
|
1475
|
-
const
|
|
1547
|
+
const callArgs = this.callResultArguments(callExpr);
|
|
1548
|
+
const resultIsCarrier = !explicitType &&
|
|
1549
|
+
this.resultCarrierArguments(returnType, terminalNode, callArgs) !== undefined;
|
|
1476
1550
|
// carrick#1877: the terminal holds a thenable the names do not peel, a
|
|
1477
1551
|
// subclass of `Promise` or a class with a `then` of its own. What a
|
|
1478
1552
|
// caller receives is what `await` yields for it, as it is for the
|
|
@@ -1521,12 +1595,22 @@ export class TypeInferrer {
|
|
|
1521
1595
|
const readThroughCarrier = unwrapResult.wasUnwrapped &&
|
|
1522
1596
|
resultIsCarrier &&
|
|
1523
1597
|
!!afterRules &&
|
|
1524
|
-
this.resultCarrierArguments(afterRules, terminalNode) === undefined;
|
|
1525
|
-
const
|
|
1598
|
+
this.resultCarrierArguments(afterRules, terminalNode, callArgs) === undefined;
|
|
1599
|
+
const carrierRead = explicitType || !afterRules
|
|
1526
1600
|
? undefined
|
|
1527
1601
|
: readThroughCarrier
|
|
1528
|
-
? afterRules
|
|
1529
|
-
: this.resultCarrierPayload(afterRules, terminalNode, use.projections, where);
|
|
1602
|
+
? { payload: afterRules }
|
|
1603
|
+
: this.resultCarrierPayload(afterRules, terminalNode, use.projections, where, callArgs);
|
|
1604
|
+
// carrick#1841: a carrier built from a response table whose success side
|
|
1605
|
+
// states no body (a `204` row), or whose side cannot be told, states no
|
|
1606
|
+
// response contract. The abstain is decided for the same reason as the
|
|
1607
|
+
// transport abstain above: left undecided, the capture's own locator
|
|
1608
|
+
// re-reads the raw call and publishes the carrier.
|
|
1609
|
+
if (carrierRead && 'abstain' in carrierRead) {
|
|
1610
|
+
this.log(`Call result at ${where}: ${carrierRead.abstain}`);
|
|
1611
|
+
return this.transportAbstain(request, callExpr, carrierRead.abstain);
|
|
1612
|
+
}
|
|
1613
|
+
const carrierCandidate = carrierRead?.payload;
|
|
1530
1614
|
// carrick#1841: what the carrier holds goes through the service's wrapper
|
|
1531
1615
|
// rules before it is published, as the call's own result did. The carrier
|
|
1532
1616
|
// is found by its shape, so what it holds can still be a library's
|
|
@@ -1658,7 +1742,7 @@ export class TypeInferrer {
|
|
|
1658
1742
|
const callAfterRules = callUnwrap.wasUnwrapped ? callUnwrap.payloadType : callPayloadType;
|
|
1659
1743
|
const leftCarrier = (callUnwrap.wasUnwrapped || readByProtocol) &&
|
|
1660
1744
|
!!callAfterRules &&
|
|
1661
|
-
this.resultCarrierArguments(callAfterRules, callExpr) !== undefined;
|
|
1745
|
+
this.resultCarrierArguments(callAfterRules, callExpr, callArgs) !== undefined;
|
|
1662
1746
|
const anchorSource = carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
|
|
1663
1747
|
let anchor = anchorSource
|
|
1664
1748
|
? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
|
|
@@ -2131,8 +2215,8 @@ export class TypeInferrer {
|
|
|
2131
2215
|
path: '',
|
|
2132
2216
|
kind: 'unknown',
|
|
2133
2217
|
reason: 'no_request_body',
|
|
2134
|
-
detail: `the
|
|
2135
|
-
'
|
|
2218
|
+
detail: `the call's request config sets no '${inCall.member}' member, which is where the call ` +
|
|
2219
|
+
'takes its body, so the call sends no request body',
|
|
2136
2220
|
},
|
|
2137
2221
|
];
|
|
2138
2222
|
return abstain;
|
|
@@ -2466,13 +2550,57 @@ export class TypeInferrer {
|
|
|
2466
2550
|
* Where neither decides, the carrier keeps its own answer and the limit is
|
|
2467
2551
|
* logged: a coin flip published as a contract is worse than an envelope a
|
|
2468
2552
|
* reader can see is an envelope.
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2553
|
+
*
|
|
2554
|
+
* carrick#1841: an argument that is a RESPONSE TABLE (`{ 200: Item }`, every
|
|
2555
|
+
* key a status code) says which side it is by its keys, and that decides
|
|
2556
|
+
* first: the one table whose keys are all 2xx is the success side, and what
|
|
2557
|
+
* a caller receives is the table's body, the member the branch holds, never
|
|
2558
|
+
* the table. A success table whose rows state no body (`{ 204: void }`)
|
|
2559
|
+
* states no response contract. A carrier built from tables, or found
|
|
2560
|
+
* through the call's arguments, that nothing decides states none either:
|
|
2561
|
+
* the union is the client's bookkeeping, not a body, so it is not published
|
|
2562
|
+
* as written.
|
|
2563
|
+
*/
|
|
2564
|
+
resultCarrierPayload(type, at, projections, where, callArgs) {
|
|
2565
|
+
const shape = this.resultCarrierArguments(type, at, callArgs);
|
|
2472
2566
|
if (!shape) {
|
|
2473
2567
|
return undefined;
|
|
2474
2568
|
}
|
|
2475
|
-
const { carrier, carried } = shape;
|
|
2569
|
+
const { carrier, carried, bodies, readOffCall } = shape;
|
|
2570
|
+
const checker = this.project.getTypeChecker().compilerObject;
|
|
2571
|
+
const tables = carried.map((arg) => {
|
|
2572
|
+
const entries = statusTableOf(checker, arg.compilerType);
|
|
2573
|
+
return {
|
|
2574
|
+
arg,
|
|
2575
|
+
entries,
|
|
2576
|
+
side: entries ? classifyStatusCodes(entries.map((entry) => entry.code)) : undefined,
|
|
2577
|
+
};
|
|
2578
|
+
});
|
|
2579
|
+
const successTables = tables.filter((table) => table.side === 'success');
|
|
2580
|
+
if (successTables.length === 1) {
|
|
2581
|
+
const { arg, entries } = successTables[0];
|
|
2582
|
+
const body = bodies.get(arg) ?? this.singleSuccessValue(arg, entries ?? [], at);
|
|
2583
|
+
if (body && statesNoBody(body.compilerType)) {
|
|
2584
|
+
return {
|
|
2585
|
+
abstain: `the call answers a carrier whose success side is the response table ` +
|
|
2586
|
+
`'${typeText(arg, at)}', and its success rows state no body, so this site ` +
|
|
2587
|
+
'states no response contract',
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
if (body) {
|
|
2591
|
+
return { payload: body };
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
const builtFromTables = tables.some((table) => table.side !== undefined);
|
|
2595
|
+
const undecided = (detail) => {
|
|
2596
|
+
this.log(`Call result at ${where} answers a carrier (${typeText(carrier, at)}): ${detail}`);
|
|
2597
|
+
return builtFromTables || readOffCall
|
|
2598
|
+
? {
|
|
2599
|
+
abstain: `the call answers a carrier (${typeText(carrier, at)}) whose success side ` +
|
|
2600
|
+
'cannot be told from its failure side, so this site states no response contract',
|
|
2601
|
+
}
|
|
2602
|
+
: undefined;
|
|
2603
|
+
};
|
|
2476
2604
|
const argTexts = new Map(carried.map((arg) => [arg.getText(), arg]));
|
|
2477
2605
|
const read = new Set();
|
|
2478
2606
|
for (const projection of projections) {
|
|
@@ -2491,27 +2619,69 @@ export class TypeInferrer {
|
|
|
2491
2619
|
// argument out than the shape test picked, the two disagree about which
|
|
2492
2620
|
// side is which and nothing here knows better than the source does.
|
|
2493
2621
|
if (byShape && byRead && byShape !== byRead) {
|
|
2494
|
-
|
|
2495
|
-
`
|
|
2496
|
-
|
|
2497
|
-
|
|
2622
|
+
return undecided(`its error shape names '${typeText(byShape, at)}' as the payload while the source ` +
|
|
2623
|
+
`reads '${typeText(byRead, at)}' out of it. Not picking one`);
|
|
2624
|
+
}
|
|
2625
|
+
const decided = byShape ?? byRead;
|
|
2626
|
+
if (decided) {
|
|
2627
|
+
return { payload: bodies.get(decided) ?? decided };
|
|
2628
|
+
}
|
|
2629
|
+
return undecided('its success side cannot be told from its failure side: no single argument is the ' +
|
|
2630
|
+
'only non-error one and the source reads none of them out. Not guessing which ' +
|
|
2631
|
+
'argument is the payload');
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* The value of a response table's one success row, for a table a carrier
|
|
2635
|
+
* holds as itself rather than through its body. `undefined` where the table
|
|
2636
|
+
* has more than one success row: their union has no single type here.
|
|
2637
|
+
*/
|
|
2638
|
+
singleSuccessValue(table, entries, at) {
|
|
2639
|
+
const success = successEntries(entries);
|
|
2640
|
+
if (success.length !== 1)
|
|
2498
2641
|
return undefined;
|
|
2642
|
+
return table.getProperty(success[0].key)?.getTypeAtLocation(at);
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* carrick#1841: the type arguments a call's result was built from, for a
|
|
2646
|
+
* result that no longer carries them. A client whose result type is a
|
|
2647
|
+
* conditional alias (`Result<TData, TError, ThrowOnError>`) resolves to its
|
|
2648
|
+
* branch at the call, and the branch is an anonymous union with no
|
|
2649
|
+
* arguments of its own. The arguments are still stated in two places the
|
|
2650
|
+
* compiler keeps:
|
|
2651
|
+
*
|
|
2652
|
+
* - the call's own explicit type arguments (`client.get<Table, Errors>(…)`);
|
|
2653
|
+
* - the alias the callee's declared return type was written with, where
|
|
2654
|
+
* the callee is a function wrapping such a call
|
|
2655
|
+
* (`getItem<ThrowOnError>(…)` returning `Result<Table, Errors, ThrowOnError>`).
|
|
2656
|
+
*
|
|
2657
|
+
* An argument read here names a payload only where a branch of the result
|
|
2658
|
+
* holds it (`resultCarrierArguments`), so a type parameter of the callee or
|
|
2659
|
+
* a style flag carries nothing.
|
|
2660
|
+
*/
|
|
2661
|
+
callResultArguments(callExpr) {
|
|
2662
|
+
const explicit = callExpr.getTypeArguments().map((node) => node.getType());
|
|
2663
|
+
if (explicit.length > 0)
|
|
2664
|
+
return explicit;
|
|
2665
|
+
try {
|
|
2666
|
+
const declaration = this.project
|
|
2667
|
+
.getTypeChecker()
|
|
2668
|
+
.getResolvedSignature(callExpr)
|
|
2669
|
+
?.getDeclaration();
|
|
2670
|
+
if (declaration && Node.isReturnTyped(declaration)) {
|
|
2671
|
+
return declaration.getReturnType().getAliasTypeArguments();
|
|
2672
|
+
}
|
|
2499
2673
|
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2674
|
+
catch {
|
|
2675
|
+
// A signature the checker cannot resolve states no arguments.
|
|
2502
2676
|
}
|
|
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;
|
|
2677
|
+
return [];
|
|
2508
2678
|
}
|
|
2509
2679
|
/**
|
|
2510
2680
|
* The shape test of `resultCarrierPayload`: the carrier `type` is, once a
|
|
2511
2681
|
* promise-like around it is peeled, and the type arguments a branch of it
|
|
2512
2682
|
* holds as a member. `undefined` when `type` is not a carrier.
|
|
2513
2683
|
*/
|
|
2514
|
-
resultCarrierArguments(type, at) {
|
|
2684
|
+
resultCarrierArguments(type, at, callArgs = []) {
|
|
2515
2685
|
const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
|
|
2516
2686
|
if (!carrier.isUnion()) {
|
|
2517
2687
|
return undefined;
|
|
@@ -2520,26 +2690,80 @@ export class TypeInferrer {
|
|
|
2520
2690
|
if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
|
|
2521
2691
|
return undefined;
|
|
2522
2692
|
}
|
|
2523
|
-
const
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2693
|
+
const own = [...carrier.getAliasTypeArguments(), ...carrier.getTypeArguments()];
|
|
2694
|
+
// carrick#1841: a carrier with no arguments of its own is read through the
|
|
2695
|
+
// arguments its call was built from (`callResultArguments`).
|
|
2696
|
+
const readOffCall = own.length === 0;
|
|
2697
|
+
const args = readOffCall ? callArgs : own;
|
|
2527
2698
|
if (args.length < 2) {
|
|
2528
2699
|
return undefined;
|
|
2529
2700
|
}
|
|
2701
|
+
// carrick#1841: on a carrier read off the call, a member every branch
|
|
2702
|
+
// holds with one type (the request and response objects a client hands
|
|
2703
|
+
// back beside the result) is the transport's, not a side of the result.
|
|
2704
|
+
const transport = readOffCall ? this.membersCommonToEveryBranch(branches, at) : new Set();
|
|
2705
|
+
const checker = this.project.getTypeChecker().compilerObject;
|
|
2706
|
+
// What a branch member holding a response table's body looks like: the
|
|
2707
|
+
// table indexed by its own keys (`Table[keyof Table]`), which the compiler
|
|
2708
|
+
// resolves to the union of the table's values.
|
|
2709
|
+
const tableValues = new Map();
|
|
2710
|
+
for (const arg of args) {
|
|
2711
|
+
const entries = statusTableOf(checker, arg.compilerType);
|
|
2712
|
+
if (entries) {
|
|
2713
|
+
tableValues.set(arg, new Set(entries.flatMap((entry) => constituents(checker.getTypeOfSymbolAtLocation(entry.symbol, at.compilerNode)))));
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
const bodies = new Map();
|
|
2530
2717
|
// A type argument only names a payload when a branch actually holds it:
|
|
2531
2718
|
// a generic that parameterises a status code or a key carries nothing.
|
|
2532
|
-
const carried = args.filter((arg) => branches.some((branch) => branch
|
|
2533
|
-
.
|
|
2534
|
-
|
|
2719
|
+
const carried = args.filter((arg) => branches.some((branch) => branch.getProperties().some((property) => {
|
|
2720
|
+
if (transport.has(property.getName()))
|
|
2721
|
+
return false;
|
|
2535
2722
|
try {
|
|
2536
|
-
|
|
2723
|
+
const held = property.getTypeAtLocation(at);
|
|
2724
|
+
if (held.getText() === arg.getText())
|
|
2725
|
+
return true;
|
|
2726
|
+
const values = tableValues.get(arg);
|
|
2727
|
+
const heldTypes = constituents(held.compilerType);
|
|
2728
|
+
if (values &&
|
|
2729
|
+
heldTypes.length === values.size &&
|
|
2730
|
+
heldTypes.every((member) => values.has(member))) {
|
|
2731
|
+
bodies.set(arg, held);
|
|
2732
|
+
return true;
|
|
2733
|
+
}
|
|
2734
|
+
return false;
|
|
2537
2735
|
}
|
|
2538
2736
|
catch {
|
|
2539
2737
|
return false;
|
|
2540
2738
|
}
|
|
2541
2739
|
})));
|
|
2542
|
-
return carried.length === 0 ? undefined : { carrier, carried };
|
|
2740
|
+
return carried.length === 0 ? undefined : { carrier, carried, bodies, readOffCall };
|
|
2741
|
+
}
|
|
2742
|
+
/** The member names every branch holds, each with one and the same type. */
|
|
2743
|
+
membersCommonToEveryBranch(branches, at) {
|
|
2744
|
+
const [first, ...rest] = branches;
|
|
2745
|
+
const common = new Set();
|
|
2746
|
+
for (const property of first.getProperties()) {
|
|
2747
|
+
const name = property.getName();
|
|
2748
|
+
let held;
|
|
2749
|
+
try {
|
|
2750
|
+
held = property.getTypeAtLocation(at);
|
|
2751
|
+
}
|
|
2752
|
+
catch {
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2755
|
+
const same = rest.every((branch) => {
|
|
2756
|
+
try {
|
|
2757
|
+
return branch.getProperty(name)?.getTypeAtLocation(at).compilerType === held.compilerType;
|
|
2758
|
+
}
|
|
2759
|
+
catch {
|
|
2760
|
+
return false;
|
|
2761
|
+
}
|
|
2762
|
+
});
|
|
2763
|
+
if (same)
|
|
2764
|
+
common.add(name);
|
|
2765
|
+
}
|
|
2766
|
+
return common;
|
|
2543
2767
|
}
|
|
2544
2768
|
/**
|
|
2545
2769
|
* The decided abstain of a call whose result carries transport the
|
|
@@ -2549,16 +2773,16 @@ export class TypeInferrer {
|
|
|
2549
2773
|
* re-reading the raw call (`inference_decided_no_contract`,
|
|
2550
2774
|
* engine/type_compat_v2.rs).
|
|
2551
2775
|
*/
|
|
2552
|
-
transportAbstain(request, callExpr
|
|
2776
|
+
transportAbstain(request, callExpr, detail = "what this call's result carries is transport that the service's wrapper " +
|
|
2777
|
+
'rules verify and read no payload out of (a library response object around the body), ' +
|
|
2778
|
+
'so this site states no response contract') {
|
|
2553
2779
|
const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(callExpr));
|
|
2554
2780
|
abstain.any_provenance = [
|
|
2555
2781
|
{
|
|
2556
2782
|
path: '',
|
|
2557
2783
|
kind: 'unknown',
|
|
2558
2784
|
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',
|
|
2785
|
+
detail,
|
|
2562
2786
|
},
|
|
2563
2787
|
];
|
|
2564
2788
|
return abstain;
|
|
@@ -3918,17 +4142,13 @@ export class TypeInferrer {
|
|
|
3918
4142
|
const isEmptyObject = (text) => text.replace(/\s+/g, '') === '{}';
|
|
3919
4143
|
const withMembers = candidates.filter((c) => !isEmptyObject(c.typeString));
|
|
3920
4144
|
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
|
-
});
|
|
4145
|
+
const distinct = distinctByText(kept, (c) => c.typeString);
|
|
3930
4146
|
const typeString = distinct.map((c) => c.typeString).join(' | ');
|
|
3931
4147
|
return {
|
|
4148
|
+
// carrick#2054: which request field picks each member of the union.
|
|
4149
|
+
responseModes: distinct.length >= 2
|
|
4150
|
+
? this.responseModesOf(kept.map((c) => ({ node: c.node, type_string: c.typeString })))
|
|
4151
|
+
: undefined,
|
|
3932
4152
|
typeString,
|
|
3933
4153
|
// Every payload node that fed the union, deduped ones included: a
|
|
3934
4154
|
// caller asks whether the expression it located is one of them.
|
|
@@ -3942,6 +4162,20 @@ export class TypeInferrer {
|
|
|
3942
4162
|
statedTypeNode: distinct.length === 1 ? distinct[0].statedTypeNode : undefined,
|
|
3943
4163
|
};
|
|
3944
4164
|
}
|
|
4165
|
+
/**
|
|
4166
|
+
* The request field each member of a joined union answers
|
|
4167
|
+
* (`response-modes.ts`). Case texts are joined by the published union's
|
|
4168
|
+
* rule. A reading that fails is no reading: it must never cost the union.
|
|
4169
|
+
*/
|
|
4170
|
+
responseModesOf(bodies) {
|
|
4171
|
+
try {
|
|
4172
|
+
return readResponseModes(bodies, (texts) => distinctByText(texts, (text) => text).join(' | '), (symbol) => this.symbolIsLibOrExternalOrigin(symbol));
|
|
4173
|
+
}
|
|
4174
|
+
catch (error) {
|
|
4175
|
+
this.log(`Response modes not read: ${error instanceof Error ? error.message : String(error)}`);
|
|
4176
|
+
return undefined;
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
3945
4179
|
/**
|
|
3946
4180
|
* The expressions a function returns, excluding returns belonging to nested
|
|
3947
4181
|
* functions (a `.map(x => …)` callback returns a row, not the response).
|
|
@@ -4270,7 +4504,7 @@ export class TypeInferrer {
|
|
|
4270
4504
|
* A status in the FIRST argument is a field of the body and is never read.
|
|
4271
4505
|
* No method or framework name is consulted anywhere.
|
|
4272
4506
|
*/
|
|
4273
|
-
responseSiteStatus(expression) {
|
|
4507
|
+
responseSiteStatus(expression, readChain = false) {
|
|
4274
4508
|
const call = this.peelTransparentExpression(expression);
|
|
4275
4509
|
if (!Node.isCallExpression(call) && !Node.isNewExpression(call)) {
|
|
4276
4510
|
return 'undecided';
|
|
@@ -4293,6 +4527,14 @@ export class TypeInferrer {
|
|
|
4293
4527
|
}
|
|
4294
4528
|
if (variable)
|
|
4295
4529
|
return 'variable';
|
|
4530
|
+
// carrick#2005: a status an earlier call of the same chain states
|
|
4531
|
+
// (`reply.status(401).json(body)`). Read only where the caller asks for
|
|
4532
|
+
// it; the return walk does not read it yet (carrick#1962).
|
|
4533
|
+
if (readChain) {
|
|
4534
|
+
const chained = this.chainStatus(call);
|
|
4535
|
+
if (chained)
|
|
4536
|
+
return chained;
|
|
4537
|
+
}
|
|
4296
4538
|
const typed = this.sendResultStatusCodes(call);
|
|
4297
4539
|
if (typed) {
|
|
4298
4540
|
const kind = classifyStatusCodes(typed);
|
|
@@ -4301,6 +4543,37 @@ export class TypeInferrer {
|
|
|
4301
4543
|
}
|
|
4302
4544
|
return 'undecided';
|
|
4303
4545
|
}
|
|
4546
|
+
/**
|
|
4547
|
+
* The status an earlier call of a send's own chain states: the receiver
|
|
4548
|
+
* calls `call` is invoked on (`a.status(401).json(body)` has `a.status(401)`),
|
|
4549
|
+
* innermost last. Each receiver call's first argument is read by the rule
|
|
4550
|
+
* an argument past a body is, so a numeric literal or a literal-typed value
|
|
4551
|
+
* counts and a plain `number` is `variable`. `undefined` when no receiver
|
|
4552
|
+
* call states a status. No method name is consulted: a receiver call that
|
|
4553
|
+
* is handed no status-shaped value says nothing.
|
|
4554
|
+
*/
|
|
4555
|
+
chainStatus(call) {
|
|
4556
|
+
if (!Node.isCallExpression(call))
|
|
4557
|
+
return undefined;
|
|
4558
|
+
let callee = call.getExpression();
|
|
4559
|
+
while (Node.isPropertyAccessExpression(callee)) {
|
|
4560
|
+
const receiver = this.peelTransparentExpression(callee.getExpression());
|
|
4561
|
+
if (!Node.isCallExpression(receiver))
|
|
4562
|
+
return undefined;
|
|
4563
|
+
const first = receiver.getArguments()[0];
|
|
4564
|
+
if (first) {
|
|
4565
|
+
const read = this.statedStatusCodes(this.peelTransparentExpression(first));
|
|
4566
|
+
if (read === 'variable')
|
|
4567
|
+
return 'variable';
|
|
4568
|
+
if (read) {
|
|
4569
|
+
const kind = classifyStatusCodes(read);
|
|
4570
|
+
return kind === 'mixed' ? 'variable' : kind;
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
callee = receiver.getExpression();
|
|
4574
|
+
}
|
|
4575
|
+
return undefined;
|
|
4576
|
+
}
|
|
4304
4577
|
/**
|
|
4305
4578
|
* The HTTP status codes an argument states, `'variable'` when it is a
|
|
4306
4579
|
* status-shaped value the source does not fix, or `undefined` when it says
|
|
@@ -4822,7 +5095,7 @@ export class TypeInferrer {
|
|
|
4822
5095
|
for (const node of sourceFile.getDescendants()) {
|
|
4823
5096
|
if ((Node.isCallExpression(node) ||
|
|
4824
5097
|
Node.isObjectLiteralExpression(node)) &&
|
|
4825
|
-
node
|
|
5098
|
+
startLineOf(node) === line) {
|
|
4826
5099
|
const handler = this.resolveRegisteredHandler(node);
|
|
4827
5100
|
if (handler) {
|
|
4828
5101
|
return { registration: node, handler };
|
|
@@ -5399,14 +5672,11 @@ export class TypeInferrer {
|
|
|
5399
5672
|
requestBodyInCall(node) {
|
|
5400
5673
|
if (Node.isCallExpression(node)) {
|
|
5401
5674
|
const args = node.getArguments();
|
|
5675
|
+
if (args.length === 1)
|
|
5676
|
+
return this.requestObjectBody(args[0]);
|
|
5402
5677
|
if (args.length < 2)
|
|
5403
5678
|
return { kind: 'none' };
|
|
5404
|
-
|
|
5405
|
-
const stringLike = first.isString() ||
|
|
5406
|
-
first.isStringLiteral() ||
|
|
5407
|
-
first.isTemplateLiteral() ||
|
|
5408
|
-
(first.isUnion() && first.getUnionTypes().every((t) => t.isString() || t.isStringLiteral()));
|
|
5409
|
-
if (!stringLike)
|
|
5679
|
+
if (!this.isStringLikeType(args[0].getType()))
|
|
5410
5680
|
return { kind: 'none' };
|
|
5411
5681
|
const slots = this.requestBodySlots(node);
|
|
5412
5682
|
for (let i = 1; i < args.length; i++) {
|
|
@@ -5442,6 +5712,80 @@ export class TypeInferrer {
|
|
|
5442
5712
|
}
|
|
5443
5713
|
return { kind: 'none' };
|
|
5444
5714
|
}
|
|
5715
|
+
/** A string, a string literal, a template literal, or a union of strings. */
|
|
5716
|
+
isStringLikeType(type) {
|
|
5717
|
+
return (type.isString() ||
|
|
5718
|
+
type.isStringLiteral() ||
|
|
5719
|
+
type.isTemplateLiteral() ||
|
|
5720
|
+
(type.isUnion() && type.getUnionTypes().every((t) => t.isString() || t.isStringLiteral())));
|
|
5721
|
+
}
|
|
5722
|
+
/**
|
|
5723
|
+
* carrick#1841, request half: a request call that states its URL on its one
|
|
5724
|
+
* object argument (`client.post({ url: '/items', ...options })`, the way a
|
|
5725
|
+
* generated client issues every operation). The method's own parameter
|
|
5726
|
+
* types the body `unknown`, so the object is the only statement of it: its
|
|
5727
|
+
* `body` member, the member `RequestInit` defines, written on the object or
|
|
5728
|
+
* carried in by a spread whose type says what it is.
|
|
5729
|
+
*
|
|
5730
|
+
* - A `body` written after the object's last spread is the body.
|
|
5731
|
+
* - Otherwise the object's own type has the answer. No `body` member, or
|
|
5732
|
+
* one typed `undefined` or `never`, means the call sends no body.
|
|
5733
|
+
* - A member typed `any`, `unknown` or a type parameter, or one only the
|
|
5734
|
+
* compiler's default library declares (a spread `RequestInit` carries the
|
|
5735
|
+
* serialised `BodyInit`, not a payload), states no payload: abstain. So
|
|
5736
|
+
* does an object that carries another body-named member and no `body`.
|
|
5737
|
+
*
|
|
5738
|
+
* An object with no string-typed `url` member is not a request object.
|
|
5739
|
+
*/
|
|
5740
|
+
requestObjectBody(arg) {
|
|
5741
|
+
const literal = this.unwrapExpressionNode(arg);
|
|
5742
|
+
if (!Node.isObjectLiteralExpression(literal))
|
|
5743
|
+
return { kind: 'none' };
|
|
5744
|
+
const objectType = literal.getType();
|
|
5745
|
+
const url = objectType.getProperty('url');
|
|
5746
|
+
if (!url || !this.isStringLikeType(url.getTypeAtLocation(literal).getNonNullableType())) {
|
|
5747
|
+
return { kind: 'none' };
|
|
5748
|
+
}
|
|
5749
|
+
const from = "the request call's request object";
|
|
5750
|
+
const member = 'body';
|
|
5751
|
+
const properties = literal.getProperties();
|
|
5752
|
+
const written = properties.findIndex((p) => (Node.isPropertyAssignment(p) || Node.isShorthandPropertyAssignment(p)) &&
|
|
5753
|
+
p.getName() === member);
|
|
5754
|
+
const spreadAfter = properties.some((p, i) => i > written && Node.isSpreadAssignment(p));
|
|
5755
|
+
if (written >= 0 && !spreadAfter) {
|
|
5756
|
+
const value = this.objectLiteralMemberValue(literal, member);
|
|
5757
|
+
if (value)
|
|
5758
|
+
return { kind: 'body', node: value, from };
|
|
5759
|
+
}
|
|
5760
|
+
const property = objectType.getProperty(member);
|
|
5761
|
+
if (!property) {
|
|
5762
|
+
const otherBodyMember = [...BODY_MEMBER_NAMES].some((name) => name !== member && objectType.getProperty(name));
|
|
5763
|
+
return otherBodyMember
|
|
5764
|
+
? { kind: 'abstain', why: `the request object carries no '${member}' member but another body-named one` }
|
|
5765
|
+
: { kind: 'no_body', member };
|
|
5766
|
+
}
|
|
5767
|
+
const type = property.getTypeAtLocation(literal);
|
|
5768
|
+
if (type.isAny() || type.isUnknown() || type.isTypeParameter()) {
|
|
5769
|
+
return { kind: 'abstain', why: `the request object's '${member}' member is not typed` };
|
|
5770
|
+
}
|
|
5771
|
+
const bare = type.getNonNullableType();
|
|
5772
|
+
if (bare.isNever() || bare.isUndefined() || bare.isVoid()) {
|
|
5773
|
+
return { kind: 'no_body', member };
|
|
5774
|
+
}
|
|
5775
|
+
if (bare.isAny() || bare.isUnknown() || bare.isTypeParameter()) {
|
|
5776
|
+
return { kind: 'abstain', why: `the request object's '${member}' member is not typed` };
|
|
5777
|
+
}
|
|
5778
|
+
const program = this.project.getProgram().compilerObject;
|
|
5779
|
+
const declarations = property.getDeclarations();
|
|
5780
|
+
if (declarations.length > 0 &&
|
|
5781
|
+
declarations.every((d) => program.isSourceFileDefaultLibrary(d.getSourceFile().compilerNode))) {
|
|
5782
|
+
return {
|
|
5783
|
+
kind: 'abstain',
|
|
5784
|
+
why: `the request object's '${member}' member is the platform's serialised body, not a payload`,
|
|
5785
|
+
};
|
|
5786
|
+
}
|
|
5787
|
+
return { kind: 'body_type', type: bare, at: literal, from };
|
|
5788
|
+
}
|
|
5445
5789
|
/**
|
|
5446
5790
|
* The body a config-slot argument supplies through `member`: an object
|
|
5447
5791
|
* literal's own member, or a variable's member read off its type.
|
|
@@ -5963,14 +6307,9 @@ export class TypeInferrer {
|
|
|
5963
6307
|
if (!Node.isPropertyAssignment(prop)) {
|
|
5964
6308
|
continue;
|
|
5965
6309
|
}
|
|
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) {
|
|
6310
|
+
// `2xx` is a status RANGE key; it reads as the bottom of its range.
|
|
6311
|
+
const code = statusKeyCode(prop.getName());
|
|
6312
|
+
if (code === undefined || code < 200 || code > 299) {
|
|
5974
6313
|
continue;
|
|
5975
6314
|
}
|
|
5976
6315
|
const initializer = prop.getInitializer();
|
|
@@ -6418,7 +6757,7 @@ export class TypeInferrer {
|
|
|
6418
6757
|
// Filter to nodes within the search window and pre-compute normalized text
|
|
6419
6758
|
const candidates = (lineNumber
|
|
6420
6759
|
? nodes.filter((node) => {
|
|
6421
|
-
const nodeLine = node
|
|
6760
|
+
const nodeLine = startLineOf(node);
|
|
6422
6761
|
return nodeLine >= lineNumber - searchRadius && nodeLine <= lineNumber + searchRadius;
|
|
6423
6762
|
})
|
|
6424
6763
|
: nodes).map((node) => ({ node, text: this.normalizeWhitespace(node.getText()) }));
|
|
@@ -6476,8 +6815,8 @@ export class TypeInferrer {
|
|
|
6476
6815
|
}
|
|
6477
6816
|
// Tie-break by proximity to target line
|
|
6478
6817
|
if (targetLine !== undefined) {
|
|
6479
|
-
const bestDist = Math.abs(best
|
|
6480
|
-
const currentDist = Math.abs(current
|
|
6818
|
+
const bestDist = Math.abs(startLineOf(best) - targetLine);
|
|
6819
|
+
const currentDist = Math.abs(startLineOf(current) - targetLine);
|
|
6481
6820
|
return currentDist < bestDist ? current : best;
|
|
6482
6821
|
}
|
|
6483
6822
|
return best;
|
|
@@ -6693,8 +7032,8 @@ export class TypeInferrer {
|
|
|
6693
7032
|
// Result Building
|
|
6694
7033
|
// ===========================================================================
|
|
6695
7034
|
getNodeLocation(node) {
|
|
6696
|
-
const startLinePos = node
|
|
6697
|
-
const endLinePos = node
|
|
7035
|
+
const startLinePos = startLineOf(node);
|
|
7036
|
+
const endLinePos = endLineOf(node);
|
|
6698
7037
|
const location = {
|
|
6699
7038
|
file_path: node.getSourceFile().getFilePath(),
|
|
6700
7039
|
start_line: startLinePos,
|
|
@@ -6716,11 +7055,11 @@ export class TypeInferrer {
|
|
|
6716
7055
|
infer_kind: request.infer_kind,
|
|
6717
7056
|
payload_type_string: payloadTypeString,
|
|
6718
7057
|
primary_type_symbol: primaryTypeSymbol,
|
|
6719
|
-
//
|
|
6720
|
-
//
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
7058
|
+
// Reported whether or not the element has a symbol (carrick#1967): an
|
|
7059
|
+
// array whose element prints structurally is still a list. The Rust
|
|
7060
|
+
// depth join copies it onto the model's symbol when the symbols agree,
|
|
7061
|
+
// or, at a handler's send, when the element has no symbol to disagree.
|
|
7062
|
+
array_depth: arrayDepth !== undefined && arrayDepth > 0 ? arrayDepth : undefined,
|
|
6724
7063
|
// Same gate: a declaration source without an anchor symbol is useless.
|
|
6725
7064
|
primary_type_symbol_source: primaryTypeSymbol !== undefined ? primaryTypeSymbolSource : undefined,
|
|
6726
7065
|
};
|