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.
Files changed (40) hide show
  1. package/dist/hook/refresh.js +8 -1
  2. package/dist/hook/refresh.js.map +1 -1
  3. package/package.json +6 -6
  4. package/plugin/.claude-plugin/plugin.json +1 -1
  5. package/sidecar/dist/src/capture/anchors.d.ts +22 -2
  6. package/sidecar/dist/src/capture/anchors.js +178 -34
  7. package/sidecar/dist/src/capture/api.d.ts +42 -1
  8. package/sidecar/dist/src/capture/check-classify.d.ts +9 -1
  9. package/sidecar/dist/src/capture/check-classify.js +15 -0
  10. package/sidecar/dist/src/capture/check-poison.js +4 -14
  11. package/sidecar/dist/src/capture/check.js +13 -1
  12. package/sidecar/dist/src/capture/index.js +55 -29
  13. package/sidecar/dist/src/capture/installed-package.d.ts +2 -0
  14. package/sidecar/dist/src/capture/installed-package.js +2 -1
  15. package/sidecar/dist/src/capture/outside-root.d.ts +19 -0
  16. package/sidecar/dist/src/capture/outside-root.js +39 -2
  17. package/sidecar/dist/src/capture/self-check.js +12 -13
  18. package/sidecar/dist/src/capture/service-config.d.ts +2 -0
  19. package/sidecar/dist/src/capture/service-config.js +1 -1
  20. package/sidecar/dist/src/capture/specifiers.d.ts +14 -0
  21. package/sidecar/dist/src/capture/specifiers.js +25 -0
  22. package/sidecar/dist/src/failure-path.d.ts +34 -3
  23. package/sidecar/dist/src/failure-path.js +59 -35
  24. package/sidecar/dist/src/function-line-index.d.ts +30 -0
  25. package/sidecar/dist/src/function-line-index.js +162 -0
  26. package/sidecar/dist/src/index.d.ts +5 -0
  27. package/sidecar/dist/src/index.js +29 -5
  28. package/sidecar/dist/src/line-index.d.ts +9 -0
  29. package/sidecar/dist/src/line-index.js +26 -0
  30. package/sidecar/dist/src/printed-names.d.ts +43 -0
  31. package/sidecar/dist/src/printed-names.js +186 -0
  32. package/sidecar/dist/src/progress.d.ts +22 -0
  33. package/sidecar/dist/src/progress.js +31 -0
  34. package/sidecar/dist/src/retype.js +58 -128
  35. package/sidecar/dist/src/type-inferrer.d.ts +124 -13
  36. package/sidecar/dist/src/type-inferrer.js +531 -146
  37. package/sidecar/dist/src/type-structural-expander.js +10 -1
  38. package/sidecar/dist/src/types.d.ts +37 -6
  39. package/sidecar/dist/src/validators.d.ts +76 -0
  40. 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
- return type.getText(enclosingNode, TYPE_TEXT_FLAGS);
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: the
243
- * scanner's read deadline for one request is 900s, and the inferences the
244
- * 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.
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
- const result = this.inferSingle(request, extractionConfig);
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
- 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);
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 wrapper rule that already unwrapped, and a type the source itself
1207
- // states, both outrank this: they are what the service's own config and
1208
- // its own author said.
1209
- 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
1210
1310
  ? undefined
1211
- : this.resultCarrierPayload(returnType, terminalNode, use.projections, `${request.file_path}:${request.line_number}`);
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 = carrierCandidate
1219
- ? this.structuralTextFromType(carrierCandidate, terminalNode)
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
- const callPayloadType = this.unwrapPromiseType(callExpr.getType());
1285
- 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
+ }
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
- const anchorSource = carrierPayload
1293
- ? carrierPayload
1294
- : callUnwrap.wasUnwrapped
1295
- ? callUnwrap.payloadType
1296
- : 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);
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 tracked binding,
1838
- // with whatever `as T` the source states around it read by
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 carrier = this.unwrapThenableType(this.unwrapPromiseType(type));
1966
- if (!carrier.isUnion()) {
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
- * `Future<T>` -> `T` for a promise-like of the source's own making, read off
2030
- * the await protocol rather than a name: a `then` whose first parameter is a
2031
- * callback, whose own first parameter is the value awaiting it yields.
2032
- * `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.
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.text()` yields a body rather than
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 read has its own branch and is taken before this
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
- bodyReadOnReceiver(identifier) {
2344
- const access = identifier.getParent();
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() !== identifier ||
2348
- access.getName() !== 'json') {
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 symbol = type.getSymbol() || type.getAliasSymbol();
2613
- const symbolName = symbol?.getName();
2614
- // 1. Check exact wrapperSymbols match. When the rule also carries
2615
- // originModuleGlobs, the symbol's declaration must come from a matching
2616
- // module — names like `Response` are shared by the DOM, frameworks, and
2617
- // HTTP clients, so a bare name match would unwrap unrelated types.
2618
- if (rule.wrapperSymbols && symbolName && rule.wrapperSymbols.includes(symbolName)) {
2619
- const originGated = !!(rule.originModuleGlobs && rule.originModuleGlobs.length > 0);
2620
- if (!originGated || this.symbolOriginatesFromModules(symbol, rule.originModuleGlobs)) {
2621
- const extracted = this.extractPayloadFromWrapper(type, node, rule, config, depth);
2622
- if (extracted) {
2623
- return { kind: 'extracted', result: extracted };
2624
- }
2625
- // A name-only match is not proof of machinery: a local type that
2626
- // happens to share the name must keep its real structural type when
2627
- // nothing was extracted. Only origin-verified matches may collapse
2628
- // to `unknown`.
2629
- 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 };
2630
2999
  }
2631
- 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' };
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
- 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)) {
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 = type.getTypeArguments();
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
- 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++) {
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. The scanner's
3753
- * pub/sub two-anchor arbitration (carrick#413) uses this to re-aim a
3754
- * demoted explicit bundle request: the bundler resolves a `SymbolRequest`
3755
- * only against declarations IN its `source_file`, so the request must
3756
- * 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.
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
- const LINE_TOLERANCE = 2;
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.