carrick 0.3.105 → 0.3.107
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/hook/refresh.js +8 -1
- package/dist/hook/refresh.js.map +1 -1
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/capture/anchors.d.ts +22 -2
- package/sidecar/dist/src/capture/anchors.js +178 -34
- package/sidecar/dist/src/capture/api.d.ts +42 -1
- package/sidecar/dist/src/capture/check-classify.d.ts +9 -1
- package/sidecar/dist/src/capture/check-classify.js +15 -0
- package/sidecar/dist/src/capture/check-poison.js +4 -14
- package/sidecar/dist/src/capture/check.js +13 -1
- package/sidecar/dist/src/capture/index.js +55 -29
- package/sidecar/dist/src/capture/installed-package.d.ts +2 -0
- package/sidecar/dist/src/capture/installed-package.js +2 -1
- package/sidecar/dist/src/capture/outside-root.d.ts +19 -0
- package/sidecar/dist/src/capture/outside-root.js +39 -2
- package/sidecar/dist/src/capture/self-check.js +12 -13
- package/sidecar/dist/src/capture/service-config.d.ts +2 -0
- package/sidecar/dist/src/capture/service-config.js +1 -1
- package/sidecar/dist/src/capture/specifiers.d.ts +14 -0
- package/sidecar/dist/src/capture/specifiers.js +25 -0
- package/sidecar/dist/src/failure-path.d.ts +34 -3
- package/sidecar/dist/src/failure-path.js +59 -35
- package/sidecar/dist/src/function-line-index.d.ts +30 -0
- package/sidecar/dist/src/function-line-index.js +162 -0
- package/sidecar/dist/src/index.d.ts +5 -0
- package/sidecar/dist/src/index.js +29 -5
- package/sidecar/dist/src/line-index.d.ts +9 -0
- package/sidecar/dist/src/line-index.js +26 -0
- package/sidecar/dist/src/printed-names.d.ts +43 -0
- package/sidecar/dist/src/printed-names.js +186 -0
- package/sidecar/dist/src/progress.d.ts +22 -0
- package/sidecar/dist/src/progress.js +31 -0
- package/sidecar/dist/src/retype.js +58 -128
- package/sidecar/dist/src/type-inferrer.d.ts +124 -13
- package/sidecar/dist/src/type-inferrer.js +531 -146
- package/sidecar/dist/src/type-structural-expander.js +10 -1
- package/sidecar/dist/src/types.d.ts +37 -6
- package/sidecar/dist/src/validators.d.ts +76 -0
- package/sidecar/dist/src/validators.js +8 -0
|
@@ -20,8 +20,10 @@
|
|
|
20
20
|
import * as path from 'node:path';
|
|
21
21
|
import { Node, SyntaxKind, ts, } from 'ts-morph';
|
|
22
22
|
import { validateInferRequestItem } from './validators.js';
|
|
23
|
+
import { notePrintedType, PrintedTypes } from './printed-names.js';
|
|
23
24
|
import { externalImportsOf, isExternalOrigin } from './origin.js';
|
|
24
25
|
import { reachedOnlyOnFailure } from './failure-path.js';
|
|
26
|
+
import { functionAtLine } from './function-line-index.js';
|
|
25
27
|
import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
|
|
26
28
|
import { expandTypeStructural, } from './type-structural-expander.js';
|
|
27
29
|
/**
|
|
@@ -220,6 +222,14 @@ const REQUEST_BODY_PARTS = new Set(['json', 'form', 'body']);
|
|
|
220
222
|
* these names; a type parameter alone also types response modes and fallbacks.
|
|
221
223
|
*/
|
|
222
224
|
const BODY_MEMBER_NAMES = new Set(['data', 'body', 'json']);
|
|
225
|
+
/**
|
|
226
|
+
* The zero-argument reads that take the WHOLE body out of a transport response
|
|
227
|
+
* (the platform `Response` and the clients that copy its shape): parsed as
|
|
228
|
+
* JSON, or as raw text (carrick#1842).
|
|
229
|
+
*/
|
|
230
|
+
const WHOLE_BODY_READS = new Set(['json', 'text']);
|
|
231
|
+
/** The body format a caller names to read a response as raw text (carrick#1842). */
|
|
232
|
+
const RAW_TEXT_FORMAT = 'text';
|
|
223
233
|
/**
|
|
224
234
|
* Print a `Type` to its string form WITHOUT the compiler's default truncation.
|
|
225
235
|
*
|
|
@@ -230,18 +240,33 @@ const BODY_MEMBER_NAMES = new Set(['data', 'body', 'json']);
|
|
|
230
240
|
*/
|
|
231
241
|
const TYPE_TEXT_FLAGS = ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias;
|
|
232
242
|
function typeText(type, enclosingNode) {
|
|
233
|
-
|
|
243
|
+
const text = type.getText(enclosingNode, TYPE_TEXT_FLAGS);
|
|
244
|
+
notePrintedType(type, enclosingNode, text);
|
|
245
|
+
return text;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* `text` is `string`, alone or beside `null` and `undefined` in either order:
|
|
249
|
+
* the only payload a raw-text read publishes (carrick#1842).
|
|
250
|
+
*/
|
|
251
|
+
function isBareStringText(text) {
|
|
252
|
+
const members = text.split('|').map((member) => member.trim());
|
|
253
|
+
return (members.includes('string') &&
|
|
254
|
+
members.every((member) => member === 'string' || member === 'null' || member === 'undefined'));
|
|
234
255
|
}
|
|
235
256
|
/**
|
|
236
257
|
* How long the unwidened reading of one batch may take by default. The
|
|
237
258
|
* reading adds a field and never an answer, so running out costs only the
|
|
238
|
-
* readings not yet made; the rewrite is always undone.
|
|
259
|
+
* readings not yet made; the rewrite is always undone. It reports no
|
|
260
|
+
* progress, so this is also the longest a batch goes silent after its last
|
|
261
|
+
* request: well inside the 900s the scanner allows between two signs of life.
|
|
239
262
|
*/
|
|
240
263
|
const UNWIDENED_BUDGET_MS = 120_000;
|
|
241
264
|
/**
|
|
242
|
-
* No reading starts or continues past this long after the batch began
|
|
243
|
-
*
|
|
244
|
-
*
|
|
265
|
+
* No reading starts or continues past this long after the batch began. The
|
|
266
|
+
* number dates from when the scanner allowed a whole request 900s; it now
|
|
267
|
+
* allows that long between progress reports (carrick#1914), so a batch can
|
|
268
|
+
* run past this and still be read. A batch that does publishes no unwidened
|
|
269
|
+
* reading, as before.
|
|
245
270
|
*/
|
|
246
271
|
const UNWIDENED_LATEST_MS = 600_000;
|
|
247
272
|
/**
|
|
@@ -286,32 +311,39 @@ export class TypeInferrer {
|
|
|
286
311
|
*
|
|
287
312
|
* @param requests - Array of inference requests
|
|
288
313
|
* @param extractionConfig - Agent-generated extraction config for payload unwrapping
|
|
314
|
+
* @param onRequestDone - Called once per request, as the batch is done with it
|
|
289
315
|
* @returns InferResult with inferred types or errors
|
|
290
316
|
*/
|
|
291
|
-
infer(requests, extractionConfig) {
|
|
317
|
+
infer(requests, extractionConfig, onRequestDone) {
|
|
292
318
|
const started = performance.now();
|
|
293
319
|
const inferredTypes = [];
|
|
294
320
|
const errors = [];
|
|
295
321
|
/** Response inferences the unwidened reading re-reads (carrick#1516). */
|
|
296
322
|
const responses = [];
|
|
297
323
|
for (const request of requests) {
|
|
298
|
-
// Plain JavaScript has no type annotations to extract, and `checkJs` is
|
|
299
|
-
// off, so inferring against a `.js` file yields nothing useful — it only
|
|
300
|
-
// crashes deep in the compiler API on undefined symbols (`escapedName`,
|
|
301
|
-
// `flags`) and floods the log with the resulting error strings. Skip it.
|
|
302
|
-
// `allowJs` stays on so `.ts` files can still resolve `.js` imports.
|
|
303
|
-
if (/\.(js|jsx|mjs|cjs)$/i.test(request.file_path)) {
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
324
|
try {
|
|
325
|
+
// Plain JavaScript has no type annotations to extract, and `checkJs` is
|
|
326
|
+
// off, so inferring against a `.js` file yields nothing useful — it only
|
|
327
|
+
// crashes deep in the compiler API on undefined symbols (`escapedName`,
|
|
328
|
+
// `flags`) and floods the log with the resulting error strings. Skip it.
|
|
329
|
+
// `allowJs` stays on so `.ts` files can still resolve `.js` imports.
|
|
330
|
+
if (/\.(js|jsx|mjs|cjs)$/i.test(request.file_path)) {
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
307
333
|
const loc = this.formatRequestLocation(request);
|
|
308
334
|
const itemError = validateInferRequestItem(request);
|
|
309
335
|
if (itemError) {
|
|
310
336
|
errors.push(`Invalid infer item at ${request.file_path}:${loc} (${request.infer_kind}): ${itemError}`);
|
|
311
337
|
continue;
|
|
312
338
|
}
|
|
313
|
-
|
|
339
|
+
// carrick#1836: what the names the published text prints meant, read
|
|
340
|
+
// while the program the prints were made in is still the project's.
|
|
341
|
+
// The unwidened re-read below records nothing: it prints the same
|
|
342
|
+
// node, and its reading has a budget.
|
|
343
|
+
const prints = new PrintedTypes();
|
|
344
|
+
const result = prints.during(() => this.inferSingle(request, extractionConfig));
|
|
314
345
|
if (result) {
|
|
346
|
+
this.recordPrintedNames(result, request, prints);
|
|
315
347
|
inferredTypes.push(result);
|
|
316
348
|
if (request.infer_kind === 'response_body' || request.infer_kind === 'function_return') {
|
|
317
349
|
responses.push({ request, result });
|
|
@@ -326,6 +358,9 @@ export class TypeInferrer {
|
|
|
326
358
|
const loc = this.formatRequestLocation(request);
|
|
327
359
|
errors.push(`Error inferring type at ${request.file_path}:${loc}: ${error}`);
|
|
328
360
|
}
|
|
361
|
+
finally {
|
|
362
|
+
onRequestDone?.();
|
|
363
|
+
}
|
|
329
364
|
}
|
|
330
365
|
try {
|
|
331
366
|
const deadline = Math.min(performance.now() + this.unwidenedBudgetMs, started + UNWIDENED_LATEST_MS);
|
|
@@ -341,6 +376,18 @@ export class TypeInferrer {
|
|
|
341
376
|
errors: errors.length > 0 ? errors : undefined,
|
|
342
377
|
};
|
|
343
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* carrick#1836: list on `result` the declarations behind the names its text
|
|
381
|
+
* prints that the request's file cannot resolve (`PrintedTypes.namesIn`).
|
|
382
|
+
*/
|
|
383
|
+
recordPrintedNames(result, request, prints) {
|
|
384
|
+
const sourceFile = this.getSourceFile(request.file_path);
|
|
385
|
+
if (!sourceFile)
|
|
386
|
+
return;
|
|
387
|
+
const printedNames = prints.namesIn([result.type_string], sourceFile.compilerNode, this.project.getTypeChecker().compilerObject);
|
|
388
|
+
if (printedNames.length > 0)
|
|
389
|
+
result.printed_names = printedNames;
|
|
390
|
+
}
|
|
344
391
|
/**
|
|
345
392
|
* carrick#1516: read each response inference again with the literals on its
|
|
346
393
|
* handler's path marked `as const`, and record the narrower type the handler
|
|
@@ -733,7 +780,7 @@ export class TypeInferrer {
|
|
|
733
780
|
return null;
|
|
734
781
|
}
|
|
735
782
|
const anchor = this.unwrapArrayLevels(awaitedType);
|
|
736
|
-
const inferred = this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(func), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth);
|
|
783
|
+
const inferred = this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(func), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth, this.primaryTypeSymbolSource(anchor.element));
|
|
737
784
|
if (provenance && provenance.length > 0) {
|
|
738
785
|
inferred.any_provenance = provenance;
|
|
739
786
|
}
|
|
@@ -975,7 +1022,7 @@ export class TypeInferrer {
|
|
|
975
1022
|
typeString = this.expandResolvedTypeStructural(resolved, typeString, this.wireFormatFor(request));
|
|
976
1023
|
}
|
|
977
1024
|
const anchor = this.unwrapArrayLevels(this.unwrapPromiseType(payloadType));
|
|
978
|
-
return this.createInferredType(request, typeString, false, this.getNodeLocation(payloadNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth);
|
|
1025
|
+
return this.createInferredType(request, typeString, false, this.getNodeLocation(payloadNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth, this.primaryTypeSymbolSource(anchor.element));
|
|
979
1026
|
}
|
|
980
1027
|
/**
|
|
981
1028
|
* True when a located call's own result is the route's payload, so the
|
|
@@ -1031,7 +1078,12 @@ export class TypeInferrer {
|
|
|
1031
1078
|
// resolved symbol is the better answer and keeps its precedence.
|
|
1032
1079
|
const stated = recovered.statedTypeNode;
|
|
1033
1080
|
const writtenAnchor = resolvedSymbol === undefined && stated ? this.writtenAnchorOf(stated) : undefined;
|
|
1034
|
-
|
|
1081
|
+
// carrick#1819: a symbol the resolved type carries has a declaration file
|
|
1082
|
+
// as much as one read off the annotation does.
|
|
1083
|
+
const resolvedSource = resolvedSymbol !== undefined && recoveredAnchor
|
|
1084
|
+
? this.primaryTypeSymbolSource(recoveredAnchor.element)
|
|
1085
|
+
: undefined;
|
|
1086
|
+
return this.createInferredType(request, recovered.typeString, recovered.isExplicit, this.getNodeLocation(recovered.node), undefined, resolvedSymbol ?? writtenAnchor?.symbol, writtenAnchor ? writtenAnchor.depth : recoveredAnchor?.depth, writtenAnchor ? writtenAnchor.source : resolvedSource);
|
|
1035
1087
|
}
|
|
1036
1088
|
/**
|
|
1037
1089
|
* carrick#1161: type a route response from every send in the handler the
|
|
@@ -1196,6 +1248,38 @@ export class TypeInferrer {
|
|
|
1196
1248
|
typeString = explicitType;
|
|
1197
1249
|
isExplicit = true;
|
|
1198
1250
|
}
|
|
1251
|
+
// carrick#1843: a rule can now match a thenable or a carrier by its alias,
|
|
1252
|
+
// so on a result that is a carrier (`Task<Outcome<Reply<T>, E>>`) the
|
|
1253
|
+
// rules reach what the carrier holds before the carrier read below does.
|
|
1254
|
+
// That changes who finds the payload, never what is published: where the
|
|
1255
|
+
// rules read through the carrier, their answer is given the way the
|
|
1256
|
+
// carrier read gives its own. A result that is no carrier keeps the
|
|
1257
|
+
// answer it had.
|
|
1258
|
+
const where = `${request.file_path}:${request.line_number}`;
|
|
1259
|
+
const resultIsCarrier = !explicitType && this.resultCarrierArguments(returnType, terminalNode) !== undefined;
|
|
1260
|
+
// carrick#1877: the terminal holds a thenable the names do not peel, a
|
|
1261
|
+
// subclass of `Promise` or a class with a `then` of its own. What a
|
|
1262
|
+
// caller receives is what `await` yields for it, as it is for the
|
|
1263
|
+
// `Promise<T>` the text unwrap at the end of this function peels: the
|
|
1264
|
+
// class is transport. A rule that matched the type as written, a type the
|
|
1265
|
+
// source states and a carrier behind the thenable have each answered
|
|
1266
|
+
// already and keep their answers.
|
|
1267
|
+
if (!unwrapResult.wasUnwrapped && !explicitType && !resultIsCarrier) {
|
|
1268
|
+
const yielded = this.awaitedBeyondPromise(returnType);
|
|
1269
|
+
if (yielded) {
|
|
1270
|
+
typeString = typeText(yielded, terminalNode);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
// The rules verified what the carrier holds as transport and read no
|
|
1274
|
+
// payload out of it (`Reply<unknown>`). That is the decision carrick#1841
|
|
1275
|
+
// makes below, and it is decided here for the same reason: left
|
|
1276
|
+
// undecided, the capture's own locator re-reads the raw call and
|
|
1277
|
+
// publishes the library's objects.
|
|
1278
|
+
if (unwrapResult.verifiedMachinery && resultIsCarrier) {
|
|
1279
|
+
this.log(`Call result at ${where} is a carrier of transport the service's wrapper rules ` +
|
|
1280
|
+
'verify and read no payload out of; this site states no response contract');
|
|
1281
|
+
return this.transportAbstain(request, callExpr);
|
|
1282
|
+
}
|
|
1199
1283
|
// carrick#1376: the call answers a RESULT CARRIER — a generic union whose
|
|
1200
1284
|
// branches say whether the call worked and carry, on the success side, the
|
|
1201
1285
|
// value it produced. The carrier is the transport's own bookkeeping; the
|
|
@@ -1203,22 +1287,67 @@ export class TypeInferrer {
|
|
|
1203
1287
|
// the carrier instead reports an envelope as a wire contract, which is the
|
|
1204
1288
|
// same class of answer the machinery guard refuses on the producer side.
|
|
1205
1289
|
//
|
|
1206
|
-
// A
|
|
1207
|
-
//
|
|
1208
|
-
//
|
|
1209
|
-
|
|
1290
|
+
// A type the source itself states outranks this: it is what the author
|
|
1291
|
+
// said. A wrapper rule that unwrapped is read first, and on a carrier it
|
|
1292
|
+
// no longer ends the matter (carrick#1843):
|
|
1293
|
+
//
|
|
1294
|
+
// - a service with a rule for `Task` and none for `Outcome` leaves
|
|
1295
|
+
// `Outcome<Reply<T>, E>`, which is as much a carrier as it was before
|
|
1296
|
+
// the rule could match. The carrier read takes what the rule left.
|
|
1297
|
+
// - a service with a rule for `Outcome` too reads the payload out of the
|
|
1298
|
+
// carrier itself. That payload is what the carrier holds, and is
|
|
1299
|
+
// published as the carrier read publishes it, so a site answers the
|
|
1300
|
+
// same whichever of the two found its payload.
|
|
1301
|
+
//
|
|
1302
|
+
// A rule that left no single payload (a union join) leaves nothing to
|
|
1303
|
+
// read.
|
|
1304
|
+
const afterRules = unwrapResult.wasUnwrapped ? unwrapResult.payloadType : returnType;
|
|
1305
|
+
const readThroughCarrier = unwrapResult.wasUnwrapped &&
|
|
1306
|
+
resultIsCarrier &&
|
|
1307
|
+
!!afterRules &&
|
|
1308
|
+
this.resultCarrierArguments(afterRules, terminalNode) === undefined;
|
|
1309
|
+
const carrierCandidate = explicitType || !afterRules
|
|
1210
1310
|
? undefined
|
|
1211
|
-
:
|
|
1311
|
+
: readThroughCarrier
|
|
1312
|
+
? afterRules
|
|
1313
|
+
: this.resultCarrierPayload(afterRules, terminalNode, use.projections, where);
|
|
1314
|
+
// carrick#1841: what the carrier holds goes through the service's wrapper
|
|
1315
|
+
// rules before it is published, as the call's own result did. The carrier
|
|
1316
|
+
// is found by its shape, so what it holds can still be a library's
|
|
1317
|
+
// envelope: a request library answers `Task<Outcome<Reply<T>, E>>`, and
|
|
1318
|
+
// `Reply<T>` is that library's response object, a status, a url and
|
|
1319
|
+
// headers around the body. Where a rule reads a payload out of it, that
|
|
1320
|
+
// payload is the body. Where a rule verifies it as the library's transport
|
|
1321
|
+
// and reads nothing out of it, this site states no contract, and the
|
|
1322
|
+
// transport object must not stand in for one: judged against a
|
|
1323
|
+
// producer's body, every member it adds reads as a field the producer
|
|
1324
|
+
// does not send.
|
|
1325
|
+
//
|
|
1326
|
+
// The abstain is decided, so it rides the row (`machinery_envelope` at the
|
|
1327
|
+
// root, no anchor). A plain `null` is re-read by the capture's own
|
|
1328
|
+
// locator, which would resolve the raw call and publish the carrier.
|
|
1329
|
+
const carrierUnwrap = carrierCandidate
|
|
1330
|
+
? this.unwrapTypeWithConfig(carrierCandidate, terminalNode, extractionConfig)
|
|
1331
|
+
: undefined;
|
|
1332
|
+
if (carrierCandidate &&
|
|
1333
|
+
carrierUnwrap?.wasUnwrapped &&
|
|
1334
|
+
carrierUnwrap.typeString.trim() === 'unknown') {
|
|
1335
|
+
const carried = typeText(carrierCandidate, terminalNode);
|
|
1336
|
+
this.log(`Call result at ${where} carries ${carried}, which the service's wrapper rules ` +
|
|
1337
|
+
'verify as transport and read no payload out of; this site states no response contract');
|
|
1338
|
+
return this.transportAbstain(request, callExpr);
|
|
1339
|
+
}
|
|
1340
|
+
const carried = carrierUnwrap?.wasUnwrapped && carrierUnwrap.payloadType
|
|
1341
|
+
? carrierUnwrap.payloadType
|
|
1342
|
+
: carrierCandidate;
|
|
1212
1343
|
// The payload rides the row as its own MEMBERS, never as its bare name
|
|
1213
1344
|
// (#257): `derive_capture_anchors` turns a usable inference into a literal
|
|
1214
1345
|
// capture anchor, and a bare name is out of scope where the surface
|
|
1215
1346
|
// declares the alias, so it would decay to a top type and publish nothing
|
|
1216
1347
|
// — trading a wrong answer for no answer. Where the payload carries no
|
|
1217
1348
|
// member shape to print, the carrier keeps its own answer.
|
|
1218
|
-
const carrierText =
|
|
1219
|
-
|
|
1220
|
-
: null;
|
|
1221
|
-
const carrierPayload = carrierText ? carrierCandidate : undefined;
|
|
1349
|
+
const carrierText = carried ? this.structuralTextFromType(carried, terminalNode) : null;
|
|
1350
|
+
const carrierPayload = carrierText ? carried : undefined;
|
|
1222
1351
|
if (carrierPayload && carrierText) {
|
|
1223
1352
|
typeString = carrierText;
|
|
1224
1353
|
}
|
|
@@ -1281,19 +1410,40 @@ export class TypeInferrer {
|
|
|
1281
1410
|
// to no single payload (union join, verified machinery) carries no
|
|
1282
1411
|
// payloadType and anchors nothing; the wrapper's own symbol must never
|
|
1283
1412
|
// anchor via that path.
|
|
1284
|
-
|
|
1285
|
-
|
|
1413
|
+
//
|
|
1414
|
+
// carrick#1877: the call's payload is what `await` yields for the call's
|
|
1415
|
+
// type. Where no rule matched that type as the names peel it, the same
|
|
1416
|
+
// protocol reading the text takes is read here, so a subclass of
|
|
1417
|
+
// `Promise` anchors the row on what it resolves to and never on itself.
|
|
1418
|
+
let callPayloadType = this.unwrapPromiseType(callExpr.getType());
|
|
1419
|
+
let callUnwrap = this.unwrapTypeWithConfig(callPayloadType, callExpr, extractionConfig);
|
|
1420
|
+
let readByProtocol = false;
|
|
1421
|
+
if (!callUnwrap.wasUnwrapped) {
|
|
1422
|
+
const yielded = this.awaitedBeyondPromise(callPayloadType);
|
|
1423
|
+
if (yielded) {
|
|
1424
|
+
callPayloadType = yielded;
|
|
1425
|
+
callUnwrap = this.unwrapTypeWithConfig(yielded, callExpr, extractionConfig);
|
|
1426
|
+
readByProtocol = true;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1286
1429
|
// carrick#1376: the carrier's own symbol must never anchor either. The
|
|
1287
1430
|
// surface pre-claims the alias from the anchor, so anchoring on the
|
|
1288
1431
|
// carrier makes the capture emit the transport's bookkeeping as the
|
|
1289
1432
|
// operation's declaration — and where the carrier is a local interface the
|
|
1290
1433
|
// service does not export, nothing is emitted at all and the row reads
|
|
1291
1434
|
// null. The payload the carrier was found to hold is the anchor.
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1435
|
+
//
|
|
1436
|
+
// carrick#1843: that holds for a carrier a rule left of the call's own
|
|
1437
|
+
// result too. A rule for `Task` alone turns `Task<Outcome<T, E>>` into
|
|
1438
|
+
// `Outcome<T, E>` here, and where the terminal's read took no payload out
|
|
1439
|
+
// of it (an open payload, or a terminal further down a chain) the
|
|
1440
|
+
// carrier's alias would otherwise be the anchor. The same holds for a
|
|
1441
|
+
// carrier the protocol reading left (carrick#1877).
|
|
1442
|
+
const callAfterRules = callUnwrap.wasUnwrapped ? callUnwrap.payloadType : callPayloadType;
|
|
1443
|
+
const leftCarrier = (callUnwrap.wasUnwrapped || readByProtocol) &&
|
|
1444
|
+
!!callAfterRules &&
|
|
1445
|
+
this.resultCarrierArguments(callAfterRules, callExpr) !== undefined;
|
|
1446
|
+
const anchorSource = carrierPayload ?? (leftCarrier ? undefined : callAfterRules);
|
|
1297
1447
|
let anchor = anchorSource
|
|
1298
1448
|
? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
|
|
1299
1449
|
: undefined;
|
|
@@ -1338,7 +1488,7 @@ export class TypeInferrer {
|
|
|
1338
1488
|
}
|
|
1339
1489
|
}
|
|
1340
1490
|
}
|
|
1341
|
-
const inferred = this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(terminalNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, anchor ? this.primaryTypeSymbol(anchor.element) : undefined, anchor?.depth);
|
|
1491
|
+
const inferred = this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(terminalNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, anchor ? this.primaryTypeSymbol(anchor.element) : undefined, anchor?.depth, anchor ? this.primaryTypeSymbolSource(anchor.element) : undefined);
|
|
1342
1492
|
// carrick#1749: say when the text is the source's own statement of the
|
|
1343
1493
|
// body, and what that statement is rooted at, so the scanner can tell a
|
|
1344
1494
|
// model symbol that names the body from one that names a part of it.
|
|
@@ -1346,8 +1496,101 @@ export class TypeInferrer {
|
|
|
1346
1496
|
if (statedBody) {
|
|
1347
1497
|
inferred.stated_body = statedBody;
|
|
1348
1498
|
}
|
|
1499
|
+
// carrick#1842: `string` cannot tell a body read as raw text from a JSON
|
|
1500
|
+
// body that is a string, and raw text states no structural contract. The
|
|
1501
|
+
// row says which it is, so the check phase can read the pair unverifiable
|
|
1502
|
+
// instead of comparing `string` with the other side's body.
|
|
1503
|
+
if (isBareStringText(typeString) &&
|
|
1504
|
+
(this.textReadAtTerminal(callExpr, terminalNode) || this.callChoosesTextBody(callExpr))) {
|
|
1505
|
+
inferred.raw_text_read = true;
|
|
1506
|
+
}
|
|
1349
1507
|
return inferred;
|
|
1350
1508
|
}
|
|
1509
|
+
/**
|
|
1510
|
+
* The terminal of a call's def-use walk is a zero-argument `.text()` read of
|
|
1511
|
+
* the call's own result (carrick#1842): on its binding (`res.text()`,
|
|
1512
|
+
* through a declaration, `await` or a cast), or in the callback a `then` on
|
|
1513
|
+
* the call hands the response to (`fetch(u).then((res) => res.text())`).
|
|
1514
|
+
*/
|
|
1515
|
+
textReadAtTerminal(callExpr, terminal) {
|
|
1516
|
+
const value = Node.isVariableDeclaration(terminal) ? terminal.getInitializer() : terminal;
|
|
1517
|
+
const read = value ? this.unwrapExpressionNode(value) : undefined;
|
|
1518
|
+
if (!read || !Node.isCallExpression(read))
|
|
1519
|
+
return false;
|
|
1520
|
+
const callResult = this.unwrapPromiseType(callExpr.getType()).compilerType;
|
|
1521
|
+
if (this.isTextReadOf(read, (receiver) => receiver === callExpr ||
|
|
1522
|
+
this.unwrapPromiseType(receiver.getType()).compilerType === callResult)) {
|
|
1523
|
+
return true;
|
|
1524
|
+
}
|
|
1525
|
+
const then = read.getExpression();
|
|
1526
|
+
if (!Node.isPropertyAccessExpression(then) ||
|
|
1527
|
+
then.getName() !== 'then' ||
|
|
1528
|
+
this.unwrapExpressionNode(then.getExpression()) !== callExpr) {
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
const callback = read.getArguments()[0];
|
|
1532
|
+
if (!callback || (!Node.isArrowFunction(callback) && !Node.isFunctionExpression(callback))) {
|
|
1533
|
+
return false;
|
|
1534
|
+
}
|
|
1535
|
+
const param = callback.getParameters()[0]?.getNameNode();
|
|
1536
|
+
if (!param || !Node.isIdentifier(param))
|
|
1537
|
+
return false;
|
|
1538
|
+
const returned = this.returnedExpressions(callback);
|
|
1539
|
+
const inner = returned.length === 1 ? this.unwrapExpressionNode(returned[0]) : undefined;
|
|
1540
|
+
return (!!inner &&
|
|
1541
|
+
Node.isCallExpression(inner) &&
|
|
1542
|
+
this.isTextReadOf(inner, (receiver) => Node.isIdentifier(receiver) && receiver.getSymbol() === param.getSymbol()));
|
|
1543
|
+
}
|
|
1544
|
+
/** `call` is `<receiver>.text()` with no arguments, and `accept` takes its receiver. */
|
|
1545
|
+
isTextReadOf(call, accept) {
|
|
1546
|
+
const access = call.getExpression();
|
|
1547
|
+
return (Node.isPropertyAccessExpression(access) &&
|
|
1548
|
+
access.getName() === RAW_TEXT_FORMAT &&
|
|
1549
|
+
call.getArguments().length === 0 &&
|
|
1550
|
+
accept(this.unwrapExpressionNode(access.getExpression())));
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* The call's resolved signature, at this site, types a member of an object
|
|
1554
|
+
* argument as exactly the literal `'text'`, and the source passes that
|
|
1555
|
+
* literal there (carrick#1842). That is how a request library lets a caller
|
|
1556
|
+
* choose a text body from the formats it offers (`{ type: 'text' }`): a
|
|
1557
|
+
* generic config instantiated by the literal, or an overload taken by it.
|
|
1558
|
+
* A member typed as a wider union (`kind: 'text' | 'image'`) chooses no
|
|
1559
|
+
* format, and no member name is read.
|
|
1560
|
+
*/
|
|
1561
|
+
callChoosesTextBody(callExpr) {
|
|
1562
|
+
const checker = this.project.getTypeChecker().compilerObject;
|
|
1563
|
+
let signature;
|
|
1564
|
+
try {
|
|
1565
|
+
signature = checker.getResolvedSignature(callExpr.compilerNode);
|
|
1566
|
+
}
|
|
1567
|
+
catch {
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
if (!signature || signature.parameters.length === 0)
|
|
1571
|
+
return false;
|
|
1572
|
+
return callExpr.getArguments().some((argument, index) => {
|
|
1573
|
+
if (!Node.isObjectLiteralExpression(argument))
|
|
1574
|
+
return false;
|
|
1575
|
+
const parameter = signature.parameters[Math.min(index, signature.parameters.length - 1)];
|
|
1576
|
+
const parameterType = checker.getTypeOfSymbolAtLocation(parameter, callExpr.compilerNode);
|
|
1577
|
+
return argument.getProperties().some((property) => {
|
|
1578
|
+
if (!Node.isPropertyAssignment(property))
|
|
1579
|
+
return false;
|
|
1580
|
+
const value = property.getInitializer();
|
|
1581
|
+
if (!value ||
|
|
1582
|
+
!(Node.isStringLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value)) ||
|
|
1583
|
+
value.getLiteralValue() !== RAW_TEXT_FORMAT) {
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
const member = checker.getPropertyOfType(parameterType, property.getName());
|
|
1587
|
+
if (!member)
|
|
1588
|
+
return false;
|
|
1589
|
+
const declared = checker.getNonNullableType(checker.getTypeOfSymbolAtLocation(member, callExpr.compilerNode));
|
|
1590
|
+
return declared.isStringLiteral() && declared.value === RAW_TEXT_FORMAT;
|
|
1591
|
+
});
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1351
1594
|
/**
|
|
1352
1595
|
* What the source states the body read at `terminal` to be, when the type
|
|
1353
1596
|
* `extractExplicitTypeFromAncestor` printed for it is stated AT the read
|
|
@@ -1459,10 +1702,12 @@ export class TypeInferrer {
|
|
|
1459
1702
|
symbol = symbol.getAliasedSymbol() ?? symbol;
|
|
1460
1703
|
}
|
|
1461
1704
|
const source = symbol?.getDeclarations()[0]?.getSourceFile().getFilePath();
|
|
1705
|
+
const typeArguments = node.getTypeArguments().length;
|
|
1462
1706
|
return {
|
|
1463
1707
|
root: nameNode.getText(),
|
|
1464
1708
|
...(source ? { root_source: source } : {}),
|
|
1465
1709
|
...depthField,
|
|
1710
|
+
...(typeArguments > 0 ? { root_type_arguments: typeArguments } : {}),
|
|
1466
1711
|
};
|
|
1467
1712
|
}
|
|
1468
1713
|
inferVariable(sourceFile, request, extractionConfig) {
|
|
@@ -1801,6 +2046,21 @@ export class TypeInferrer {
|
|
|
1801
2046
|
return { terminal: returnExpr, projectionOnly: false, projections: [] };
|
|
1802
2047
|
}
|
|
1803
2048
|
}
|
|
2049
|
+
// carrick#1851: the source reads the body in place on the call's own
|
|
2050
|
+
// value, `const body = await (await fetch(url)).text()`. No binding holds
|
|
2051
|
+
// the response, so the walk below has nothing to follow, and the call
|
|
2052
|
+
// itself would stand as the terminal and publish the transport object.
|
|
2053
|
+
// The read is the terminal, as it is when the walk finds it on a binding,
|
|
2054
|
+
// and a call that takes the read and states what it returns says more
|
|
2055
|
+
// than the read does (carrick#1382).
|
|
2056
|
+
const inPlaceRead = this.bodyReadOnCallValue(callExpr);
|
|
2057
|
+
if (inPlaceRead) {
|
|
2058
|
+
return {
|
|
2059
|
+
terminal: this.statedPayloadAroundBodyRead(inPlaceRead) ?? inPlaceRead,
|
|
2060
|
+
projectionOnly: false,
|
|
2061
|
+
projections: [],
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
1804
2064
|
const binding = this.extractBindingFromCall(callExpr);
|
|
1805
2065
|
if (binding && func) {
|
|
1806
2066
|
let currentNames = binding.names;
|
|
@@ -1834,9 +2094,9 @@ export class TypeInferrer {
|
|
|
1834
2094
|
continue;
|
|
1835
2095
|
}
|
|
1836
2096
|
// The body a caller reads OUT of a transport response IS the payload
|
|
1837
|
-
// of the call that produced it: `res.json()` on the
|
|
1838
|
-
// with whatever `as T` the source states around it
|
|
1839
|
-
// `extractExplicitTypeFromAncestor` below. A bare identifier never
|
|
2097
|
+
// of the call that produced it: `res.json()` or `res.text()` on the
|
|
2098
|
+
// tracked binding, with whatever `as T` the source states around it
|
|
2099
|
+
// read by `extractExplicitTypeFromAncestor` below. A bare identifier never
|
|
1840
2100
|
// matches `expressionUsesNames` (it tests DESCENDANTS), so without
|
|
1841
2101
|
// this the body read is invisible to the def-use walk.
|
|
1842
2102
|
if (Node.isIdentifier(expr) && this.isIdentifierUsage(expr, currentNames)) {
|
|
@@ -1962,36 +2222,11 @@ export class TypeInferrer {
|
|
|
1962
2222
|
* reader can see is an envelope.
|
|
1963
2223
|
*/
|
|
1964
2224
|
resultCarrierPayload(type, at, projections, where) {
|
|
1965
|
-
const
|
|
1966
|
-
if (!
|
|
1967
|
-
return undefined;
|
|
1968
|
-
}
|
|
1969
|
-
const branches = carrier.getUnionTypes();
|
|
1970
|
-
if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
|
|
1971
|
-
return undefined;
|
|
1972
|
-
}
|
|
1973
|
-
const args = [
|
|
1974
|
-
...carrier.getAliasTypeArguments(),
|
|
1975
|
-
...carrier.getTypeArguments(),
|
|
1976
|
-
];
|
|
1977
|
-
if (args.length < 2) {
|
|
1978
|
-
return undefined;
|
|
1979
|
-
}
|
|
1980
|
-
// A type argument only names a payload when a branch actually holds it:
|
|
1981
|
-
// a generic that parameterises a status code or a key carries nothing.
|
|
1982
|
-
const carried = args.filter((arg) => branches.some((branch) => branch
|
|
1983
|
-
.getProperties()
|
|
1984
|
-
.some((property) => {
|
|
1985
|
-
try {
|
|
1986
|
-
return property.getTypeAtLocation(at).getText() === arg.getText();
|
|
1987
|
-
}
|
|
1988
|
-
catch {
|
|
1989
|
-
return false;
|
|
1990
|
-
}
|
|
1991
|
-
})));
|
|
1992
|
-
if (carried.length === 0) {
|
|
2225
|
+
const shape = this.resultCarrierArguments(type, at);
|
|
2226
|
+
if (!shape) {
|
|
1993
2227
|
return undefined;
|
|
1994
2228
|
}
|
|
2229
|
+
const { carrier, carried } = shape;
|
|
1995
2230
|
const argTexts = new Map(carried.map((arg) => [arg.getText(), arg]));
|
|
1996
2231
|
const read = new Set();
|
|
1997
2232
|
for (const projection of projections) {
|
|
@@ -2026,12 +2261,102 @@ export class TypeInferrer {
|
|
|
2026
2261
|
return undefined;
|
|
2027
2262
|
}
|
|
2028
2263
|
/**
|
|
2029
|
-
*
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
2032
|
-
|
|
2264
|
+
* The shape test of `resultCarrierPayload`: the carrier `type` is, once a
|
|
2265
|
+
* promise-like around it is peeled, and the type arguments a branch of it
|
|
2266
|
+
* holds as a member. `undefined` when `type` is not a carrier.
|
|
2267
|
+
*/
|
|
2268
|
+
resultCarrierArguments(type, at) {
|
|
2269
|
+
const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
|
|
2270
|
+
if (!carrier.isUnion()) {
|
|
2271
|
+
return undefined;
|
|
2272
|
+
}
|
|
2273
|
+
const branches = carrier.getUnionTypes();
|
|
2274
|
+
if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
|
|
2275
|
+
return undefined;
|
|
2276
|
+
}
|
|
2277
|
+
const args = [
|
|
2278
|
+
...carrier.getAliasTypeArguments(),
|
|
2279
|
+
...carrier.getTypeArguments(),
|
|
2280
|
+
];
|
|
2281
|
+
if (args.length < 2) {
|
|
2282
|
+
return undefined;
|
|
2283
|
+
}
|
|
2284
|
+
// A type argument only names a payload when a branch actually holds it:
|
|
2285
|
+
// a generic that parameterises a status code or a key carries nothing.
|
|
2286
|
+
const carried = args.filter((arg) => branches.some((branch) => branch
|
|
2287
|
+
.getProperties()
|
|
2288
|
+
.some((property) => {
|
|
2289
|
+
try {
|
|
2290
|
+
return property.getTypeAtLocation(at).getText() === arg.getText();
|
|
2291
|
+
}
|
|
2292
|
+
catch {
|
|
2293
|
+
return false;
|
|
2294
|
+
}
|
|
2295
|
+
})));
|
|
2296
|
+
return carried.length === 0 ? undefined : { carrier, carried };
|
|
2297
|
+
}
|
|
2298
|
+
/**
|
|
2299
|
+
* The decided abstain of a call whose result carries transport the
|
|
2300
|
+
* service's wrapper rules verify and read no payload out of (carrick#1841,
|
|
2301
|
+
* carrick#1843): `unknown` with `machinery_envelope` at the root and no
|
|
2302
|
+
* anchor. The root reason is what keeps the capture's own locator from
|
|
2303
|
+
* re-reading the raw call (`inference_decided_no_contract`,
|
|
2304
|
+
* engine/type_compat_v2.rs).
|
|
2305
|
+
*/
|
|
2306
|
+
transportAbstain(request, callExpr) {
|
|
2307
|
+
const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(callExpr));
|
|
2308
|
+
abstain.any_provenance = [
|
|
2309
|
+
{
|
|
2310
|
+
path: '',
|
|
2311
|
+
kind: 'unknown',
|
|
2312
|
+
reason: 'machinery_envelope',
|
|
2313
|
+
detail: "what this call's result carries is transport that the service's wrapper rules " +
|
|
2314
|
+
'verify and read no payload out of (a library response object around the body), ' +
|
|
2315
|
+
'so this site states no response contract',
|
|
2316
|
+
},
|
|
2317
|
+
];
|
|
2318
|
+
return abstain;
|
|
2319
|
+
}
|
|
2320
|
+
/**
|
|
2321
|
+
* `Future<T>` -> `T` for a thenable, read off the await protocol rather
|
|
2322
|
+
* than a name: a `then` whose first parameter is a callback, whose own
|
|
2323
|
+
* first parameter is the value awaiting it yields. `Promise` and
|
|
2324
|
+
* `PromiseLike` are peeled by `unwrapPromiseType` before this.
|
|
2325
|
+
*
|
|
2326
|
+
* The callback is read through `null` and `undefined` (carrick#1877). A
|
|
2327
|
+
* `then` of the source's own making declares `(value: T) => void`; the
|
|
2328
|
+
* platform's declares `onfulfilled?: ((value: T) => ...) | null`, and that
|
|
2329
|
+
* is the `then` a subclass of `Promise` inherits. Read as written, an
|
|
2330
|
+
* optional, nullable callback has no call signature, and the subclass was
|
|
2331
|
+
* not seen as a thenable at all.
|
|
2332
|
+
*
|
|
2333
|
+
* A `then` that cannot be called, or whose first parameter is no callback,
|
|
2334
|
+
* is no protocol, and the type is returned as it is.
|
|
2335
|
+
*
|
|
2336
|
+
* The compiler's own awaited type arbitrates. This walk reads the first
|
|
2337
|
+
* signature of `then`; the language reads all of them. Where awaiting the
|
|
2338
|
+
* type and awaiting what this walk found are not the same thing to the
|
|
2339
|
+
* compiler (an overloaded `then` whose first signature is not the one
|
|
2340
|
+
* `await` takes), the walk did not read the protocol, and the type is
|
|
2341
|
+
* returned as it is rather than published as a guess.
|
|
2033
2342
|
*/
|
|
2034
2343
|
unwrapThenableType(type) {
|
|
2344
|
+
const yielded = this.firstThenValue(type);
|
|
2345
|
+
if (yielded === type)
|
|
2346
|
+
return type;
|
|
2347
|
+
try {
|
|
2348
|
+
const checker = this.project.getTypeChecker().compilerObject;
|
|
2349
|
+
const awaited = checker.getAwaitedType(type.compilerType);
|
|
2350
|
+
return awaited !== undefined && awaited === checker.getAwaitedType(yielded.compilerType)
|
|
2351
|
+
? yielded
|
|
2352
|
+
: type;
|
|
2353
|
+
}
|
|
2354
|
+
catch {
|
|
2355
|
+
return type;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
/** The value `then`'s first signature hands its callback, read until it stops changing. */
|
|
2359
|
+
firstThenValue(type) {
|
|
2035
2360
|
let current = type;
|
|
2036
2361
|
for (let depth = 0; depth < 8; depth++) {
|
|
2037
2362
|
const then = current.getProperty('then');
|
|
@@ -2044,7 +2369,8 @@ export class TypeInferrer {
|
|
|
2044
2369
|
.getTypeAtLocation(declaration)
|
|
2045
2370
|
.getCallSignatures()[0]
|
|
2046
2371
|
?.getParameters()[0]
|
|
2047
|
-
?.getTypeAtLocation(declaration)
|
|
2372
|
+
?.getTypeAtLocation(declaration)
|
|
2373
|
+
.getNonNullableType();
|
|
2048
2374
|
}
|
|
2049
2375
|
catch {
|
|
2050
2376
|
return current;
|
|
@@ -2059,6 +2385,18 @@ export class TypeInferrer {
|
|
|
2059
2385
|
}
|
|
2060
2386
|
return current;
|
|
2061
2387
|
}
|
|
2388
|
+
/**
|
|
2389
|
+
* What `await` yields for `type` where the names do not say: `type` is,
|
|
2390
|
+
* once `Promise` and `PromiseLike` are peeled, a thenable by the protocol
|
|
2391
|
+
* (a subclass of `Promise`, a class with a `then` of its own). `undefined`
|
|
2392
|
+
* where the names say it all or `type` is no thenable, so a caller keeps
|
|
2393
|
+
* the reading it had (carrick#1877).
|
|
2394
|
+
*/
|
|
2395
|
+
awaitedBeyondPromise(type) {
|
|
2396
|
+
const named = this.unwrapPromiseType(type);
|
|
2397
|
+
const yielded = this.unwrapThenableType(named);
|
|
2398
|
+
return yielded === named ? undefined : yielded;
|
|
2399
|
+
}
|
|
2062
2400
|
/**
|
|
2063
2401
|
* The platform's error shape, in full: `name` and `message` strings AND a
|
|
2064
2402
|
* `stack`, which is what the `Error` interface declares and every subclass
|
|
@@ -2094,10 +2432,10 @@ export class TypeInferrer {
|
|
|
2094
2432
|
* `query.data`, `envelope` in `envelope.list[0]` — or `undefined` when the
|
|
2095
2433
|
* identifier names the value itself.
|
|
2096
2434
|
*
|
|
2097
|
-
* A member CALL is not a projection: `res.
|
|
2435
|
+
* A member CALL is not a projection: `res.blob()` yields a body rather than
|
|
2098
2436
|
* a part of one, and what it returns stays the walk's business. The
|
|
2099
|
-
* zero-argument json body
|
|
2100
|
-
* is asked.
|
|
2437
|
+
* zero-argument json and text body reads have their own branch and are
|
|
2438
|
+
* taken before this is asked.
|
|
2101
2439
|
*/
|
|
2102
2440
|
projectionOnReceiver(identifier) {
|
|
2103
2441
|
const access = identifier.getParent();
|
|
@@ -2340,12 +2678,42 @@ export class TypeInferrer {
|
|
|
2340
2678
|
!this.leavesAPositionOpen(statedNode));
|
|
2341
2679
|
});
|
|
2342
2680
|
}
|
|
2343
|
-
|
|
2344
|
-
|
|
2681
|
+
/**
|
|
2682
|
+
* The zero-argument whole-body read taken in place on the value `callExpr`
|
|
2683
|
+
* yields, or `undefined` (carrick#1851): `(await fetch(url)).text()`. The
|
|
2684
|
+
* receiver is the call itself, through the wrappers that leave a value as
|
|
2685
|
+
* it is (parentheses, `await`, `!`), so it is the same read
|
|
2686
|
+
* `bodyReadOnReceiver` finds on a binding of that value. A call that is not
|
|
2687
|
+
* awaited first is read the same way: a request that is a promise and reads
|
|
2688
|
+
* its own body (`send(url).json()`) yields the body from that read too.
|
|
2689
|
+
*/
|
|
2690
|
+
bodyReadOnCallValue(callExpr) {
|
|
2691
|
+
let value = callExpr;
|
|
2692
|
+
for (;;) {
|
|
2693
|
+
const parent = value.getParent();
|
|
2694
|
+
if (!parent ||
|
|
2695
|
+
!(Node.isParenthesizedExpression(parent) ||
|
|
2696
|
+
Node.isAwaitExpression(parent) ||
|
|
2697
|
+
Node.isNonNullExpression(parent)) ||
|
|
2698
|
+
parent.getExpression() !== value) {
|
|
2699
|
+
break;
|
|
2700
|
+
}
|
|
2701
|
+
value = parent;
|
|
2702
|
+
}
|
|
2703
|
+
return this.bodyReadOnReceiver(value);
|
|
2704
|
+
}
|
|
2705
|
+
/**
|
|
2706
|
+
* The zero-argument whole-body read that takes `receiver` as its receiver,
|
|
2707
|
+
* `res.json()` or `res.text()`, or `undefined`. A text read is a body read
|
|
2708
|
+
* like a json one (carrick#1842): without it, `return res.text()` left the
|
|
2709
|
+
* walk on the response binding and published the transport object.
|
|
2710
|
+
*/
|
|
2711
|
+
bodyReadOnReceiver(receiver) {
|
|
2712
|
+
const access = receiver.getParent();
|
|
2345
2713
|
if (!access ||
|
|
2346
2714
|
!Node.isPropertyAccessExpression(access) ||
|
|
2347
|
-
access.getExpression() !==
|
|
2348
|
-
access.getName()
|
|
2715
|
+
access.getExpression() !== receiver ||
|
|
2716
|
+
!WHOLE_BODY_READS.has(access.getName())) {
|
|
2349
2717
|
return undefined;
|
|
2350
2718
|
}
|
|
2351
2719
|
const call = access.getParent();
|
|
@@ -2609,26 +2977,31 @@ export class TypeInferrer {
|
|
|
2609
2977
|
if (depth >= maxDepth) {
|
|
2610
2978
|
return { kind: 'no-match' };
|
|
2611
2979
|
}
|
|
2612
|
-
const
|
|
2613
|
-
|
|
2614
|
-
//
|
|
2615
|
-
//
|
|
2616
|
-
//
|
|
2617
|
-
//
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2980
|
+
const readings = this.wrapperReadings(type);
|
|
2981
|
+
// 1. Check exact wrapperSymbols match, against either name the type goes
|
|
2982
|
+
// by (carrick#1843). When the rule also carries originModuleGlobs, the
|
|
2983
|
+
// declaration of the name that matched must come from a matching module
|
|
2984
|
+
// — names like `Response` are shared by the DOM, frameworks, and HTTP
|
|
2985
|
+
// clients, so a bare name match would unwrap unrelated types. A name
|
|
2986
|
+
// that fails that gate is no match, and the rule's test of members and
|
|
2987
|
+
// origin below still has its turn: a service's alias that shares the
|
|
2988
|
+
// rule's name can stand around the very type the rule verifies.
|
|
2989
|
+
const named = readings.filter((reading) => rule.wrapperSymbols?.includes(reading.symbol.getName()));
|
|
2990
|
+
const globs = rule.originModuleGlobs ?? [];
|
|
2991
|
+
const originGated = globs.length > 0;
|
|
2992
|
+
const matched = originGated
|
|
2993
|
+
? named.find((reading) => this.symbolOriginatesFromModules(reading.symbol, globs))
|
|
2994
|
+
: named[0];
|
|
2995
|
+
if (matched) {
|
|
2996
|
+
const extracted = this.extractPayloadFromWrapper(type, matched, node, rule, config, depth);
|
|
2997
|
+
if (extracted) {
|
|
2998
|
+
return { kind: 'extracted', result: extracted };
|
|
2630
2999
|
}
|
|
2631
|
-
|
|
3000
|
+
// A name-only match is not proof of machinery: a local type that
|
|
3001
|
+
// happens to share the name must keep its real structural type when
|
|
3002
|
+
// nothing was extracted. Only origin-verified matches may collapse
|
|
3003
|
+
// to `unknown`.
|
|
3004
|
+
return originGated ? { kind: 'verified-no-payload' } : { kind: 'no-match' };
|
|
2632
3005
|
}
|
|
2633
3006
|
// 2. Check machineryIndicators + originModuleGlobs. Indicators alone are
|
|
2634
3007
|
// too many false positives, so the origin gate is mandatory here — which
|
|
@@ -2640,10 +3013,18 @@ export class TypeInferrer {
|
|
|
2640
3013
|
if (!this.typeHasMachineryIndicators(type, rule.machineryIndicators)) {
|
|
2641
3014
|
return { kind: 'no-match' };
|
|
2642
3015
|
}
|
|
2643
|
-
|
|
3016
|
+
// The rule named neither of the type's names, so this branch reads
|
|
3017
|
+
// what it always read: the first symbol the type has, and the type's
|
|
3018
|
+
// own arguments. An alias the rule did not name says nothing about
|
|
3019
|
+
// where a payload is, whoever declares it (carrick#1843):
|
|
3020
|
+
// `Omit<Response, 'json'>` is written through a library alias whose
|
|
3021
|
+
// first argument is the transport itself, and a service's own alias
|
|
3022
|
+
// around a library's object orders its parameters as it likes.
|
|
3023
|
+
const symbol = type.getSymbol() || type.getAliasSymbol();
|
|
3024
|
+
if (!symbol || !this.symbolOriginatesFromModules(symbol, rule.originModuleGlobs)) {
|
|
2644
3025
|
return { kind: 'no-match' };
|
|
2645
3026
|
}
|
|
2646
|
-
const extracted = this.extractPayloadFromWrapper(type, node, rule, config, depth);
|
|
3027
|
+
const extracted = this.extractPayloadFromWrapper(type, { symbol, typeArguments: type.getTypeArguments(), viaAlias: false }, node, rule, config, depth);
|
|
2647
3028
|
if (extracted) {
|
|
2648
3029
|
return { kind: 'extracted', result: extracted };
|
|
2649
3030
|
}
|
|
@@ -2651,14 +3032,44 @@ export class TypeInferrer {
|
|
|
2651
3032
|
}
|
|
2652
3033
|
return { kind: 'no-match' };
|
|
2653
3034
|
}
|
|
3035
|
+
/**
|
|
3036
|
+
* The names `type` goes by, own symbol first (carrick#1843).
|
|
3037
|
+
*
|
|
3038
|
+
* `type Task<A> = __Task<A>` has the class's symbol and arguments, and the
|
|
3039
|
+
* alias's beside them. `type Reply<T> = { ... }` has the anonymous `__type`
|
|
3040
|
+
* with no arguments of its own. `type Outcome<A, E> = Done<A, E> |
|
|
3041
|
+
* Failed<A, E>` has no symbol of its own at all. In each, the alias and its
|
|
3042
|
+
* arguments are what a rule naming `Task`, `Reply` or `Outcome` describes.
|
|
3043
|
+
*/
|
|
3044
|
+
wrapperReadings(type) {
|
|
3045
|
+
const readings = [];
|
|
3046
|
+
const own = type.getSymbol();
|
|
3047
|
+
if (own) {
|
|
3048
|
+
readings.push({ symbol: own, typeArguments: type.getTypeArguments(), viaAlias: false });
|
|
3049
|
+
}
|
|
3050
|
+
const alias = type.getAliasSymbol();
|
|
3051
|
+
if (alias && alias !== own) {
|
|
3052
|
+
readings.push({
|
|
3053
|
+
symbol: alias,
|
|
3054
|
+
typeArguments: type.getAliasTypeArguments(),
|
|
3055
|
+
viaAlias: true,
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
return readings;
|
|
3059
|
+
}
|
|
2654
3060
|
/**
|
|
2655
3061
|
* Extract the payload type from a matched wrapper. Returns null when the
|
|
2656
3062
|
* rule matched the wrapper but no payload is recoverable from generics or
|
|
2657
3063
|
* property paths — the caller decides what a payload-less match means
|
|
2658
3064
|
* (verified machinery collapses to `unknown` after every rule has run;
|
|
2659
3065
|
* a name-only match leaves the type untouched).
|
|
3066
|
+
*
|
|
3067
|
+
* `payloadGenericIndex` counts the arguments of `reading`, the name the
|
|
3068
|
+
* rule matched (carrick#1843): an alias is free to order its parameters
|
|
3069
|
+
* differently from the type it stands for, so the same index into the
|
|
3070
|
+
* other list is a different argument.
|
|
2660
3071
|
*/
|
|
2661
|
-
extractPayloadFromWrapper(type, node, rule, config, depth) {
|
|
3072
|
+
extractPayloadFromWrapper(type, reading, node, rule, config, depth) {
|
|
2662
3073
|
// The outer extraction already succeeded on the paths below; a recursive
|
|
2663
3074
|
// inner pass that finds nothing more must not demote the result back to
|
|
2664
3075
|
// "not unwrapped" (which would discard the recovered payload). Only when
|
|
@@ -2677,7 +3088,7 @@ export class TypeInferrer {
|
|
|
2677
3088
|
};
|
|
2678
3089
|
// 1. Try generic type argument at payloadGenericIndex
|
|
2679
3090
|
const genericIndex = rule.payloadGenericIndex ?? 0;
|
|
2680
|
-
const typeArgs =
|
|
3091
|
+
const typeArgs = reading.typeArguments;
|
|
2681
3092
|
if (typeArgs.length > genericIndex) {
|
|
2682
3093
|
const payloadArg = typeArgs[genericIndex];
|
|
2683
3094
|
// Check if it's a useful type (not any/unknown/never)
|
|
@@ -2694,8 +3105,11 @@ export class TypeInferrer {
|
|
|
2694
3105
|
payloadType: payloadArg,
|
|
2695
3106
|
};
|
|
2696
3107
|
}
|
|
2697
|
-
// Try "first useful generic" heuristic
|
|
2698
|
-
|
|
3108
|
+
// Try "first useful generic" heuristic. It is a guess about a type's
|
|
3109
|
+
// own arguments and is not extended to an alias's (carrick#1843): the
|
|
3110
|
+
// alias this reaches most often is a result carrier, `Outcome<A, E>`,
|
|
3111
|
+
// whose next argument after an open payload is the error side.
|
|
3112
|
+
for (let i = 0; !reading.viaAlias && i < typeArgs.length; i++) {
|
|
2699
3113
|
const argType = typeArgs[i];
|
|
2700
3114
|
const text = typeText(argType, node);
|
|
2701
3115
|
if (!this.isUselessType(text)) {
|
|
@@ -3749,11 +4163,19 @@ export class TypeInferrer {
|
|
|
3749
4163
|
/**
|
|
3750
4164
|
* Declaration file (absolute path) of the anchor symbol
|
|
3751
4165
|
* `primaryTypeSymbol` reports for this type, or `undefined` when the type
|
|
3752
|
-
* has no user-facing anchor or no source declaration.
|
|
3753
|
-
*
|
|
3754
|
-
*
|
|
3755
|
-
*
|
|
3756
|
-
*
|
|
4166
|
+
* has no user-facing anchor or no source declaration. Every path that
|
|
4167
|
+
* reports the symbol reports this beside it (carrick#1819): it is where a
|
|
4168
|
+
* reader finds the type an anchor names. The scanner's pub/sub two-anchor
|
|
4169
|
+
* arbitration (carrick#413) also uses it to re-aim a demoted explicit
|
|
4170
|
+
* bundle request: the bundler resolves a `SymbolRequest` only against
|
|
4171
|
+
* declarations IN its `source_file`, so the request must point at the file
|
|
4172
|
+
* that actually declares the tsc-witnessed payload type.
|
|
4173
|
+
*
|
|
4174
|
+
* The declarations read are those of the type's own symbol, so a name
|
|
4175
|
+
* imported through a barrel reports the file that declares it, and a name
|
|
4176
|
+
* two files declare reports the one this type resolves to. A declaration in
|
|
4177
|
+
* an installed package or a TypeScript lib is reported as it is found, as
|
|
4178
|
+
* an absolute path; what a reader is shown for it is the scanner's call.
|
|
3757
4179
|
*
|
|
3758
4180
|
* Only declaration kinds the bundler's `validateSymbols` can resolve
|
|
3759
4181
|
* (interface, type alias, class, enum, function, variable) count. A
|
|
@@ -4073,44 +4495,7 @@ export class TypeInferrer {
|
|
|
4073
4495
|
* which is the honest answer and the one a consumer check can act on.
|
|
4074
4496
|
*/
|
|
4075
4497
|
findFunctionByLine(sourceFile, line) {
|
|
4076
|
-
|
|
4077
|
-
const functions = [];
|
|
4078
|
-
/** Statements opening inside the forward window, in source order. */
|
|
4079
|
-
const windowStatements = [];
|
|
4080
|
-
for (const node of sourceFile.getDescendants()) {
|
|
4081
|
-
if (Node.isFunctionDeclaration(node) ||
|
|
4082
|
-
Node.isArrowFunction(node) ||
|
|
4083
|
-
Node.isFunctionExpression(node) ||
|
|
4084
|
-
Node.isMethodDeclaration(node)) {
|
|
4085
|
-
functions.push(node);
|
|
4086
|
-
}
|
|
4087
|
-
if (Node.isStatement(node)) {
|
|
4088
|
-
const start = node.getStartLineNumber();
|
|
4089
|
-
if (start >= line && start <= line + LINE_TOLERANCE) {
|
|
4090
|
-
windowStatements.push(node);
|
|
4091
|
-
}
|
|
4092
|
-
}
|
|
4093
|
-
}
|
|
4094
|
-
const separatedFromAnchor = (fn) => windowStatements.some((statement) => statement.getStartLineNumber() < fn.getStartLineNumber() &&
|
|
4095
|
-
!(statement.getStart() <= fn.getStart() && statement.getEnd() >= fn.getEnd()));
|
|
4096
|
-
let best;
|
|
4097
|
-
let bestDelta = Infinity;
|
|
4098
|
-
for (const fn of functions) {
|
|
4099
|
-
const delta = Math.abs(fn.getStartLineNumber() - line);
|
|
4100
|
-
if (delta > LINE_TOLERANCE)
|
|
4101
|
-
continue;
|
|
4102
|
-
if (fn.getStartLineNumber() > line && separatedFromAnchor(fn))
|
|
4103
|
-
continue;
|
|
4104
|
-
const isCloser = delta < bestDelta;
|
|
4105
|
-
const isInnermostTie = delta === bestDelta &&
|
|
4106
|
-
best !== undefined &&
|
|
4107
|
-
fn.getEnd() - fn.getStart() < best.getEnd() - best.getStart();
|
|
4108
|
-
if (isCloser || isInnermostTie) {
|
|
4109
|
-
best = fn;
|
|
4110
|
-
bestDelta = delta;
|
|
4111
|
-
}
|
|
4112
|
-
}
|
|
4113
|
-
return best;
|
|
4498
|
+
return functionAtLine(sourceFile, line);
|
|
4114
4499
|
}
|
|
4115
4500
|
/**
|
|
4116
4501
|
* Walk up from a node to find its innermost containing function.
|