carrick 0.3.106 → 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.
@@ -23,6 +23,7 @@ import { validateInferRequestItem } from './validators.js';
23
23
  import { notePrintedType, PrintedTypes } from './printed-names.js';
24
24
  import { externalImportsOf, isExternalOrigin } from './origin.js';
25
25
  import { reachedOnlyOnFailure } from './failure-path.js';
26
+ import { functionAtLine } from './function-line-index.js';
26
27
  import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
27
28
  import { expandTypeStructural, } from './type-structural-expander.js';
28
29
  /**
@@ -255,13 +256,17 @@ function isBareStringText(text) {
255
256
  /**
256
257
  * How long the unwidened reading of one batch may take by default. The
257
258
  * reading adds a field and never an answer, so running out costs only the
258
- * 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.
259
262
  */
260
263
  const UNWIDENED_BUDGET_MS = 120_000;
261
264
  /**
262
- * No reading starts or continues past this long after the batch began: the
263
- * scanner's read deadline for one request is 900s, and the inferences the
264
- * batch already made must reach it.
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.
265
270
  */
266
271
  const UNWIDENED_LATEST_MS = 600_000;
267
272
  /**
@@ -306,24 +311,25 @@ export class TypeInferrer {
306
311
  *
307
312
  * @param requests - Array of inference requests
308
313
  * @param extractionConfig - Agent-generated extraction config for payload unwrapping
314
+ * @param onRequestDone - Called once per request, as the batch is done with it
309
315
  * @returns InferResult with inferred types or errors
310
316
  */
311
- infer(requests, extractionConfig) {
317
+ infer(requests, extractionConfig, onRequestDone) {
312
318
  const started = performance.now();
313
319
  const inferredTypes = [];
314
320
  const errors = [];
315
321
  /** Response inferences the unwidened reading re-reads (carrick#1516). */
316
322
  const responses = [];
317
323
  for (const request of requests) {
318
- // Plain JavaScript has no type annotations to extract, and `checkJs` is
319
- // off, so inferring against a `.js` file yields nothing useful — it only
320
- // crashes deep in the compiler API on undefined symbols (`escapedName`,
321
- // `flags`) and floods the log with the resulting error strings. Skip it.
322
- // `allowJs` stays on so `.ts` files can still resolve `.js` imports.
323
- if (/\.(js|jsx|mjs|cjs)$/i.test(request.file_path)) {
324
- continue;
325
- }
326
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
+ }
327
333
  const loc = this.formatRequestLocation(request);
328
334
  const itemError = validateInferRequestItem(request);
329
335
  if (itemError) {
@@ -352,6 +358,9 @@ export class TypeInferrer {
352
358
  const loc = this.formatRequestLocation(request);
353
359
  errors.push(`Error inferring type at ${request.file_path}:${loc}: ${error}`);
354
360
  }
361
+ finally {
362
+ onRequestDone?.();
363
+ }
355
364
  }
356
365
  try {
357
366
  const deadline = Math.min(performance.now() + this.unwidenedBudgetMs, started + UNWIDENED_LATEST_MS);
@@ -771,7 +780,7 @@ export class TypeInferrer {
771
780
  return null;
772
781
  }
773
782
  const anchor = this.unwrapArrayLevels(awaitedType);
774
- 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));
775
784
  if (provenance && provenance.length > 0) {
776
785
  inferred.any_provenance = provenance;
777
786
  }
@@ -1013,7 +1022,7 @@ export class TypeInferrer {
1013
1022
  typeString = this.expandResolvedTypeStructural(resolved, typeString, this.wireFormatFor(request));
1014
1023
  }
1015
1024
  const anchor = this.unwrapArrayLevels(this.unwrapPromiseType(payloadType));
1016
- 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));
1017
1026
  }
1018
1027
  /**
1019
1028
  * True when a located call's own result is the route's payload, so the
@@ -1069,7 +1078,12 @@ export class TypeInferrer {
1069
1078
  // resolved symbol is the better answer and keeps its precedence.
1070
1079
  const stated = recovered.statedTypeNode;
1071
1080
  const writtenAnchor = resolvedSymbol === undefined && stated ? this.writtenAnchorOf(stated) : undefined;
1072
- return this.createInferredType(request, recovered.typeString, recovered.isExplicit, this.getNodeLocation(recovered.node), undefined, resolvedSymbol ?? writtenAnchor?.symbol, writtenAnchor ? writtenAnchor.depth : recoveredAnchor?.depth, writtenAnchor?.source);
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);
1073
1087
  }
1074
1088
  /**
1075
1089
  * carrick#1161: type a route response from every send in the handler the
@@ -1234,6 +1248,38 @@ export class TypeInferrer {
1234
1248
  typeString = explicitType;
1235
1249
  isExplicit = true;
1236
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
+ }
1237
1283
  // carrick#1376: the call answers a RESULT CARRIER — a generic union whose
1238
1284
  // branches say whether the call worked and carry, on the success side, the
1239
1285
  // value it produced. The carrier is the transport's own bookkeeping; the
@@ -1241,12 +1287,30 @@ export class TypeInferrer {
1241
1287
  // the carrier instead reports an envelope as a wire contract, which is the
1242
1288
  // same class of answer the machinery guard refuses on the producer side.
1243
1289
  //
1244
- // A wrapper rule that already unwrapped, and a type the source itself
1245
- // states, both outrank this: they are what the service's own config and
1246
- // its own author said.
1247
- const carrierCandidate = unwrapResult.wasUnwrapped || explicitType
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
1248
1310
  ? undefined
1249
- : this.resultCarrierPayload(returnType, terminalNode, use.projections, `${request.file_path}:${request.line_number}`);
1311
+ : readThroughCarrier
1312
+ ? afterRules
1313
+ : this.resultCarrierPayload(afterRules, terminalNode, use.projections, where);
1250
1314
  // carrick#1841: what the carrier holds goes through the service's wrapper
1251
1315
  // rules before it is published, as the call's own result did. The carrier
1252
1316
  // is found by its shape, so what it holds can still be a library's
@@ -1269,21 +1333,9 @@ export class TypeInferrer {
1269
1333
  carrierUnwrap?.wasUnwrapped &&
1270
1334
  carrierUnwrap.typeString.trim() === 'unknown') {
1271
1335
  const carried = typeText(carrierCandidate, terminalNode);
1272
- this.log(`Call result at ${request.file_path}:${request.line_number} carries ${carried}, which ` +
1273
- "the service's wrapper rules verify as transport and read no payload out of; this " +
1274
- 'site states no response contract');
1275
- const abstain = this.createInferredType(request, 'unknown', false, this.getNodeLocation(callExpr));
1276
- abstain.any_provenance = [
1277
- {
1278
- path: '',
1279
- kind: 'unknown',
1280
- reason: 'machinery_envelope',
1281
- detail: "what this call's result carries is transport that the service's wrapper rules " +
1282
- 'verify and read no payload out of (a library response object around the body), ' +
1283
- 'so this site states no response contract',
1284
- },
1285
- ];
1286
- return abstain;
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);
1287
1339
  }
1288
1340
  const carried = carrierUnwrap?.wasUnwrapped && carrierUnwrap.payloadType
1289
1341
  ? carrierUnwrap.payloadType
@@ -1358,19 +1410,40 @@ export class TypeInferrer {
1358
1410
  // to no single payload (union join, verified machinery) carries no
1359
1411
  // payloadType and anchors nothing; the wrapper's own symbol must never
1360
1412
  // anchor via that path.
1361
- const callPayloadType = this.unwrapPromiseType(callExpr.getType());
1362
- const callUnwrap = this.unwrapTypeWithConfig(callPayloadType, callExpr, extractionConfig);
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
+ }
1363
1429
  // carrick#1376: the carrier's own symbol must never anchor either. The
1364
1430
  // surface pre-claims the alias from the anchor, so anchoring on the
1365
1431
  // carrier makes the capture emit the transport's bookkeeping as the
1366
1432
  // operation's declaration — and where the carrier is a local interface the
1367
1433
  // service does not export, nothing is emitted at all and the row reads
1368
1434
  // null. The payload the carrier was found to hold is the anchor.
1369
- const anchorSource = carrierPayload
1370
- ? carrierPayload
1371
- : callUnwrap.wasUnwrapped
1372
- ? callUnwrap.payloadType
1373
- : callPayloadType;
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);
1374
1447
  let anchor = anchorSource
1375
1448
  ? this.unwrapArrayLevels(this.unwrapPromiseType(anchorSource))
1376
1449
  : undefined;
@@ -1415,7 +1488,7 @@ export class TypeInferrer {
1415
1488
  }
1416
1489
  }
1417
1490
  }
1418
- 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);
1419
1492
  // carrick#1749: say when the text is the source's own statement of the
1420
1493
  // body, and what that statement is rooted at, so the scanner can tell a
1421
1494
  // model symbol that names the body from one that names a part of it.
@@ -1973,6 +2046,21 @@ export class TypeInferrer {
1973
2046
  return { terminal: returnExpr, projectionOnly: false, projections: [] };
1974
2047
  }
1975
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
+ }
1976
2064
  const binding = this.extractBindingFromCall(callExpr);
1977
2065
  if (binding && func) {
1978
2066
  let currentNames = binding.names;
@@ -2134,36 +2222,11 @@ export class TypeInferrer {
2134
2222
  * reader can see is an envelope.
2135
2223
  */
2136
2224
  resultCarrierPayload(type, at, projections, where) {
2137
- const carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
2138
- if (!carrier.isUnion()) {
2139
- return undefined;
2140
- }
2141
- const branches = carrier.getUnionTypes();
2142
- if (branches.length < 2 || !branches.every((branch) => this.isObjectShape(branch))) {
2143
- return undefined;
2144
- }
2145
- const args = [
2146
- ...carrier.getAliasTypeArguments(),
2147
- ...carrier.getTypeArguments(),
2148
- ];
2149
- if (args.length < 2) {
2150
- return undefined;
2151
- }
2152
- // A type argument only names a payload when a branch actually holds it:
2153
- // a generic that parameterises a status code or a key carries nothing.
2154
- const carried = args.filter((arg) => branches.some((branch) => branch
2155
- .getProperties()
2156
- .some((property) => {
2157
- try {
2158
- return property.getTypeAtLocation(at).getText() === arg.getText();
2159
- }
2160
- catch {
2161
- return false;
2162
- }
2163
- })));
2164
- if (carried.length === 0) {
2225
+ const shape = this.resultCarrierArguments(type, at);
2226
+ if (!shape) {
2165
2227
  return undefined;
2166
2228
  }
2229
+ const { carrier, carried } = shape;
2167
2230
  const argTexts = new Map(carried.map((arg) => [arg.getText(), arg]));
2168
2231
  const read = new Set();
2169
2232
  for (const projection of projections) {
@@ -2198,12 +2261,102 @@ export class TypeInferrer {
2198
2261
  return undefined;
2199
2262
  }
2200
2263
  /**
2201
- * `Future<T>` -> `T` for a promise-like of the source's own making, read off
2202
- * the await protocol rather than a name: a `then` whose first parameter is a
2203
- * callback, whose own first parameter is the value awaiting it yields.
2204
- * `Promise` and `PromiseLike` are peeled by `unwrapPromiseType` before this.
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.
2205
2342
  */
2206
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) {
2207
2360
  let current = type;
2208
2361
  for (let depth = 0; depth < 8; depth++) {
2209
2362
  const then = current.getProperty('then');
@@ -2216,7 +2369,8 @@ export class TypeInferrer {
2216
2369
  .getTypeAtLocation(declaration)
2217
2370
  .getCallSignatures()[0]
2218
2371
  ?.getParameters()[0]
2219
- ?.getTypeAtLocation(declaration);
2372
+ ?.getTypeAtLocation(declaration)
2373
+ .getNonNullableType();
2220
2374
  }
2221
2375
  catch {
2222
2376
  return current;
@@ -2231,6 +2385,18 @@ export class TypeInferrer {
2231
2385
  }
2232
2386
  return current;
2233
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
+ }
2234
2400
  /**
2235
2401
  * The platform's error shape, in full: `name` and `message` strings AND a
2236
2402
  * `stack`, which is what the `Error` interface declares and every subclass
@@ -2513,16 +2679,40 @@ export class TypeInferrer {
2513
2679
  });
2514
2680
  }
2515
2681
  /**
2516
- * The zero-argument whole-body read that takes `identifier` as its receiver,
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,
2517
2707
  * `res.json()` or `res.text()`, or `undefined`. A text read is a body read
2518
2708
  * like a json one (carrick#1842): without it, `return res.text()` left the
2519
2709
  * walk on the response binding and published the transport object.
2520
2710
  */
2521
- bodyReadOnReceiver(identifier) {
2522
- const access = identifier.getParent();
2711
+ bodyReadOnReceiver(receiver) {
2712
+ const access = receiver.getParent();
2523
2713
  if (!access ||
2524
2714
  !Node.isPropertyAccessExpression(access) ||
2525
- access.getExpression() !== identifier ||
2715
+ access.getExpression() !== receiver ||
2526
2716
  !WHOLE_BODY_READS.has(access.getName())) {
2527
2717
  return undefined;
2528
2718
  }
@@ -2787,26 +2977,31 @@ export class TypeInferrer {
2787
2977
  if (depth >= maxDepth) {
2788
2978
  return { kind: 'no-match' };
2789
2979
  }
2790
- const symbol = type.getSymbol() || type.getAliasSymbol();
2791
- const symbolName = symbol?.getName();
2792
- // 1. Check exact wrapperSymbols match. When the rule also carries
2793
- // originModuleGlobs, the symbol's declaration must come from a matching
2794
- // module — names like `Response` are shared by the DOM, frameworks, and
2795
- // HTTP clients, so a bare name match would unwrap unrelated types.
2796
- if (rule.wrapperSymbols && symbolName && rule.wrapperSymbols.includes(symbolName)) {
2797
- const originGated = !!(rule.originModuleGlobs && rule.originModuleGlobs.length > 0);
2798
- if (!originGated || this.symbolOriginatesFromModules(symbol, rule.originModuleGlobs)) {
2799
- const extracted = this.extractPayloadFromWrapper(type, node, rule, config, depth);
2800
- if (extracted) {
2801
- return { kind: 'extracted', result: extracted };
2802
- }
2803
- // A name-only match is not proof of machinery: a local type that
2804
- // happens to share the name must keep its real structural type when
2805
- // nothing was extracted. Only origin-verified matches may collapse
2806
- // to `unknown`.
2807
- return originGated ? { kind: 'verified-no-payload' } : { kind: 'no-match' };
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 };
2808
2999
  }
2809
- return { kind: 'no-match' };
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' };
2810
3005
  }
2811
3006
  // 2. Check machineryIndicators + originModuleGlobs. Indicators alone are
2812
3007
  // too many false positives, so the origin gate is mandatory here — which
@@ -2818,10 +3013,18 @@ export class TypeInferrer {
2818
3013
  if (!this.typeHasMachineryIndicators(type, rule.machineryIndicators)) {
2819
3014
  return { kind: 'no-match' };
2820
3015
  }
2821
- if (!this.symbolOriginatesFromModules(symbol, rule.originModuleGlobs)) {
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)) {
2822
3025
  return { kind: 'no-match' };
2823
3026
  }
2824
- 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);
2825
3028
  if (extracted) {
2826
3029
  return { kind: 'extracted', result: extracted };
2827
3030
  }
@@ -2829,14 +3032,44 @@ export class TypeInferrer {
2829
3032
  }
2830
3033
  return { kind: 'no-match' };
2831
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
+ }
2832
3060
  /**
2833
3061
  * Extract the payload type from a matched wrapper. Returns null when the
2834
3062
  * rule matched the wrapper but no payload is recoverable from generics or
2835
3063
  * property paths — the caller decides what a payload-less match means
2836
3064
  * (verified machinery collapses to `unknown` after every rule has run;
2837
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.
2838
3071
  */
2839
- extractPayloadFromWrapper(type, node, rule, config, depth) {
3072
+ extractPayloadFromWrapper(type, reading, node, rule, config, depth) {
2840
3073
  // The outer extraction already succeeded on the paths below; a recursive
2841
3074
  // inner pass that finds nothing more must not demote the result back to
2842
3075
  // "not unwrapped" (which would discard the recovered payload). Only when
@@ -2855,7 +3088,7 @@ export class TypeInferrer {
2855
3088
  };
2856
3089
  // 1. Try generic type argument at payloadGenericIndex
2857
3090
  const genericIndex = rule.payloadGenericIndex ?? 0;
2858
- const typeArgs = type.getTypeArguments();
3091
+ const typeArgs = reading.typeArguments;
2859
3092
  if (typeArgs.length > genericIndex) {
2860
3093
  const payloadArg = typeArgs[genericIndex];
2861
3094
  // Check if it's a useful type (not any/unknown/never)
@@ -2872,8 +3105,11 @@ export class TypeInferrer {
2872
3105
  payloadType: payloadArg,
2873
3106
  };
2874
3107
  }
2875
- // Try "first useful generic" heuristic
2876
- for (let i = 0; i < typeArgs.length; i++) {
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++) {
2877
3113
  const argType = typeArgs[i];
2878
3114
  const text = typeText(argType, node);
2879
3115
  if (!this.isUselessType(text)) {
@@ -3927,11 +4163,19 @@ export class TypeInferrer {
3927
4163
  /**
3928
4164
  * Declaration file (absolute path) of the anchor symbol
3929
4165
  * `primaryTypeSymbol` reports for this type, or `undefined` when the type
3930
- * has no user-facing anchor or no source declaration. The scanner's
3931
- * pub/sub two-anchor arbitration (carrick#413) uses this to re-aim a
3932
- * demoted explicit bundle request: the bundler resolves a `SymbolRequest`
3933
- * only against declarations IN its `source_file`, so the request must
3934
- * point at the file that actually declares the tsc-witnessed payload type.
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.
3935
4179
  *
3936
4180
  * Only declaration kinds the bundler's `validateSymbols` can resolve
3937
4181
  * (interface, type alias, class, enum, function, variable) count. A
@@ -4251,44 +4495,7 @@ export class TypeInferrer {
4251
4495
  * which is the honest answer and the one a consumer check can act on.
4252
4496
  */
4253
4497
  findFunctionByLine(sourceFile, line) {
4254
- const LINE_TOLERANCE = 2;
4255
- const functions = [];
4256
- /** Statements opening inside the forward window, in source order. */
4257
- const windowStatements = [];
4258
- for (const node of sourceFile.getDescendants()) {
4259
- if (Node.isFunctionDeclaration(node) ||
4260
- Node.isArrowFunction(node) ||
4261
- Node.isFunctionExpression(node) ||
4262
- Node.isMethodDeclaration(node)) {
4263
- functions.push(node);
4264
- }
4265
- if (Node.isStatement(node)) {
4266
- const start = node.getStartLineNumber();
4267
- if (start >= line && start <= line + LINE_TOLERANCE) {
4268
- windowStatements.push(node);
4269
- }
4270
- }
4271
- }
4272
- const separatedFromAnchor = (fn) => windowStatements.some((statement) => statement.getStartLineNumber() < fn.getStartLineNumber() &&
4273
- !(statement.getStart() <= fn.getStart() && statement.getEnd() >= fn.getEnd()));
4274
- let best;
4275
- let bestDelta = Infinity;
4276
- for (const fn of functions) {
4277
- const delta = Math.abs(fn.getStartLineNumber() - line);
4278
- if (delta > LINE_TOLERANCE)
4279
- continue;
4280
- if (fn.getStartLineNumber() > line && separatedFromAnchor(fn))
4281
- continue;
4282
- const isCloser = delta < bestDelta;
4283
- const isInnermostTie = delta === bestDelta &&
4284
- best !== undefined &&
4285
- fn.getEnd() - fn.getStart() < best.getEnd() - best.getStart();
4286
- if (isCloser || isInnermostTie) {
4287
- best = fn;
4288
- bestDelta = delta;
4289
- }
4290
- }
4291
- return best;
4498
+ return functionAtLine(sourceFile, line);
4292
4499
  }
4293
4500
  /**
4294
4501
  * Walk up from a node to find its innermost containing function.