carrick 0.3.102 → 0.3.104

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 (45) hide show
  1. package/package.json +6 -6
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/sidecar/dist/src/bundler.d.ts +3 -92
  4. package/sidecar/dist/src/bundler.js +5 -265
  5. package/sidecar/dist/src/capture/anchors.js +113 -8
  6. package/sidecar/dist/src/capture/api.d.ts +6 -0
  7. package/sidecar/dist/src/capture/check-classify.js +27 -0
  8. package/sidecar/dist/src/capture/check-fields.js +19 -5
  9. package/sidecar/dist/src/capture/check-probe.d.ts +6 -1
  10. package/sidecar/dist/src/capture/check-probe.js +45 -2
  11. package/sidecar/dist/src/capture/check-workspace.d.ts +3 -0
  12. package/sidecar/dist/src/capture/check-workspace.js +13 -16
  13. package/sidecar/dist/src/capture/check.js +5 -5
  14. package/sidecar/dist/src/capture/deep-walk.js +45 -12
  15. package/sidecar/dist/src/capture/deno-project.d.ts +2 -1
  16. package/sidecar/dist/src/capture/deno-project.js +22 -18
  17. package/sidecar/dist/src/capture/guarded-fs.d.ts +76 -0
  18. package/sidecar/dist/src/capture/guarded-fs.js +182 -0
  19. package/sidecar/dist/src/capture/index.d.ts +1 -0
  20. package/sidecar/dist/src/capture/index.js +65 -27
  21. package/sidecar/dist/src/capture/outside-root.d.ts +41 -0
  22. package/sidecar/dist/src/capture/outside-root.js +101 -0
  23. package/sidecar/dist/src/capture/paths-rewrite.d.ts +8 -0
  24. package/sidecar/dist/src/capture/paths-rewrite.js +9 -7
  25. package/sidecar/dist/src/capture/repair-dangling.d.ts +11 -1
  26. package/sidecar/dist/src/capture/repair-dangling.js +15 -4
  27. package/sidecar/dist/src/capture/self-check.d.ts +3 -0
  28. package/sidecar/dist/src/capture/self-check.js +3 -3
  29. package/sidecar/dist/src/capture/service-config.d.ts +19 -0
  30. package/sidecar/dist/src/capture/service-config.js +57 -0
  31. package/sidecar/dist/src/index.d.ts +1 -1
  32. package/sidecar/dist/src/index.js +4 -115
  33. package/sidecar/dist/src/origin.d.ts +49 -8
  34. package/sidecar/dist/src/origin.js +81 -8
  35. package/sidecar/dist/src/project-loader.d.ts +10 -2
  36. package/sidecar/dist/src/project-loader.js +22 -21
  37. package/sidecar/dist/src/type-inferrer.d.ts +72 -0
  38. package/sidecar/dist/src/type-inferrer.js +248 -8
  39. package/sidecar/dist/src/type-structural-expander.d.ts +5 -1
  40. package/sidecar/dist/src/type-structural-expander.js +1 -1
  41. package/sidecar/dist/src/types.d.ts +39 -178
  42. package/sidecar/dist/src/validators.d.ts +70 -914
  43. package/sidecar/dist/src/validators.js +2 -55
  44. package/sidecar/dist/src/monorepo-builder.d.ts +0 -129
  45. package/sidecar/dist/src/monorepo-builder.js +0 -584
@@ -12,8 +12,9 @@
12
12
  import { Project } from 'ts-morph';
13
13
  import * as path from 'node:path';
14
14
  import * as fs from 'node:fs';
15
- import { DenoProject, findDenoConfig, serviceConfigPath } from './capture/index.js';
15
+ import { DenoProject, findDenoConfig, findServiceTsconfig, serviceConfigPath } from './capture/index.js';
16
16
  import { moduleFormatResolutionHost } from './module-format.js';
17
+ import { ExternalImports, registerExternalImports } from './origin.js';
17
18
  /**
18
19
  * Source-file patterns used when the repo declares no tsconfig, relative to
19
20
  * the repo root. `node_modules` is excluded explicitly: a glob that matches
@@ -124,6 +125,7 @@ export class ProjectLoader {
124
125
  ownerProjects = new Map();
125
126
  repoRoot;
126
127
  tsconfigPath;
128
+ scanRoot;
127
129
  tsconfigSnapshot;
128
130
  pinnedDependencies;
129
131
  initialized = false;
@@ -140,6 +142,7 @@ export class ProjectLoader {
140
142
  ? options.tsconfigPath
141
143
  : path.resolve(this.repoRoot, options.tsconfigPath);
142
144
  }
145
+ this.scanRoot = options.scanRoot;
143
146
  // Store snapshot if provided
144
147
  this.tsconfigSnapshot = options.tsconfigSnapshot;
145
148
  this.pinnedDependencies = options.pinnedDependencies;
@@ -190,14 +193,18 @@ export class ProjectLoader {
190
193
  this.buildProject = () => {
191
194
  const deno = new DenoProject(denoConfig, this.repoRoot);
192
195
  this.denoProject = deno;
196
+ // The graph's external-library verdicts, kept past the program
197
+ // rebuilds that drop them (carrick#1731).
198
+ const imports = new ExternalImports();
193
199
  const project = new Project({
194
200
  compilerOptions: deno.parsed.options,
195
201
  skipAddingFilesFromTsConfig: true,
196
- resolutionHost: (host, getOptions) => ({
202
+ resolutionHost: imports.recording((host, getOptions) => ({
197
203
  resolveModuleNames: (names, from) => names.map(name => deno.resolve(name, from, getOptions(), host)),
198
204
  resolveTypeReferenceDirectives: (names, from) => names.map(name => deno.resolveTypeReference(typeof name === 'string' ? name : name.fileName, from, getOptions(), host)),
199
- }),
205
+ })),
200
206
  });
207
+ registerExternalImports(project, imports);
201
208
  for (const file of deno.parsed.fileNames)
202
209
  project.addSourceFileAtPath(file);
203
210
  for (const diagnostic of deno.diagnostics)
@@ -313,12 +320,17 @@ export class ProjectLoader {
313
320
  }
314
321
  /** A ts-morph project built from one tsconfig and the files it lists. */
315
322
  projectFromConfig(configPath) {
316
- return new Project({
323
+ const imports = new ExternalImports();
324
+ const project = new Project({
317
325
  tsConfigFilePath: configPath,
318
326
  skipAddingFilesFromTsConfig: false,
319
- // Each import resolves in its file's own module format (carrick#1619).
320
- resolutionHost: moduleFormatResolutionHost,
327
+ // Each import resolves in its file's own module format (carrick#1619),
328
+ // and every external-library answer is kept past the program rebuilds
329
+ // that drop it (carrick#1731).
330
+ resolutionHost: imports.recording(moduleFormatResolutionHost),
321
331
  });
332
+ registerExternalImports(project, imports);
333
+ return project;
322
334
  }
323
335
  /**
324
336
  * Which project types `file` (carrick#1604): the key of its owning
@@ -412,30 +424,19 @@ export class ProjectLoader {
412
424
  return this.pinnedDependencies;
413
425
  }
414
426
  /**
415
- * Find the tsconfig.json file to use
427
+ * The tsconfig to build from: the named one when it exists, else the one
428
+ * `findServiceTsconfig` finds for the service, which capture also reads.
416
429
  *
417
- * @returns Absolute path to tsconfig.json, or undefined if not found
430
+ * @returns Absolute path to the tsconfig, or undefined if not found
418
431
  */
419
432
  findTsConfig() {
420
- // If a specific path was provided, try to use it
421
433
  if (this.tsconfigPath) {
422
434
  if (fs.existsSync(this.tsconfigPath)) {
423
435
  return this.tsconfigPath;
424
436
  }
425
437
  this.log(`Specified tsconfig not found: ${this.tsconfigPath}`);
426
438
  }
427
- // Try common tsconfig locations
428
- const candidates = [
429
- path.join(this.repoRoot, 'tsconfig.json'),
430
- path.join(this.repoRoot, 'tsconfig.build.json'),
431
- path.join(this.repoRoot, 'tsconfig.app.json'),
432
- ];
433
- for (const candidate of candidates) {
434
- if (fs.existsSync(candidate)) {
435
- return candidate;
436
- }
437
- }
438
- return undefined;
439
+ return findServiceTsconfig(this.repoRoot, this.scanRoot);
439
440
  }
440
441
  /**
441
442
  * Add source files from common project locations when no tsconfig is found.
@@ -182,6 +182,25 @@ export declare class TypeInferrer {
182
182
  */
183
183
  private resolveParamTarget;
184
184
  private inferResponseBody;
185
+ /**
186
+ * True when a located call's own result is the route's payload, so the
187
+ * transitional drill into its first argument must not run (carrick#1732).
188
+ *
189
+ * `res.json(users)` reached that drill because nothing above it recognised
190
+ * the send: its result reads `void`, `any` or `unknown`, and the payload is
191
+ * the argument. `toPublicView(row)` is the opposite case: a mapper building
192
+ * the object the route sends. Its first argument is the row it was built
193
+ * FROM, which carries columns the route never sends.
194
+ *
195
+ * The call's result decides, not where its callee is declared. It is the
196
+ * payload when it reads as one by the rule a response helper's argument is
197
+ * read with (`nodeCarriesPayloadContract`: object-shaped, not machinery,
198
+ * not `void`/`any`/`unknown`) and the object is not a library's own: a
199
+ * codec's writer from `encode(message)` or a reply builder from a send is
200
+ * the library describing itself, and keeps the drill. A library call that
201
+ * returns the repo's own type (`toInstance(View, plain)`) is the payload.
202
+ */
203
+ private callResultIsPayload;
185
204
  /**
186
205
  * The wire representation a request's printed type takes. A route response
187
206
  * is serialised as JSON by every sender this layer reads a payload out of,
@@ -238,6 +257,30 @@ export declare class TypeInferrer {
238
257
  */
239
258
  private receivingCallOf;
240
259
  private inferCallResult;
260
+ /**
261
+ * What the source states the body read at `terminal` to be, when the type
262
+ * `extractExplicitTypeFromAncestor` printed for it is stated AT the read
263
+ * (carrick#1749): the read is the operand of that cast, or the initializer
264
+ * of that annotated declaration, through wrappers that leave a value as it
265
+ * is (parentheses, `await`, `!`, another cast). An annotation further out —
266
+ * the declared type of the function the read sits in — describes something
267
+ * else, and is not reported.
268
+ */
269
+ private statedBodyAtRead;
270
+ /**
271
+ * A stated type with `any` or `unknown` written anywhere in it (`unknown`,
272
+ * `Record<string, unknown>`, `{ items: any[] }`) leaves a position open: it
273
+ * is a placeholder the source narrows later (`const data: unknown = await
274
+ * res.json()`, then `data as Entry[]`), not its statement of the body.
275
+ */
276
+ private leavesAPositionOpen;
277
+ /**
278
+ * The named root of a stated body type: `Promise<...>` and `PromiseLike`
279
+ * (the language's await protocol), array levels, parentheses, `readonly`
280
+ * and `| null`/`| undefined` are peeled, and what is left is either a type
281
+ * reference, whose name is the root, or anything else, which has none.
282
+ */
283
+ private statedRoot;
241
284
  private inferVariable;
242
285
  private inferExpression;
243
286
  private inferRequestBody;
@@ -361,6 +404,33 @@ export declare class TypeInferrer {
361
404
  * HTTP response is read exactly this way whatever produced the response, so
362
405
  * the shape is structural, not a framework's name.
363
406
  */
407
+ /**
408
+ * The body read inside a chain that starts at `callExpr` (carrick#1749):
409
+ * `request(url).check(ok).mapOk(response => { const data = response as
410
+ * SearchResponse; ... })`.
411
+ *
412
+ * The chain is the run of member calls whose receiver is the call, then
413
+ * that call's result, and so on. A callback passed to one of them whose
414
+ * first parameter the compiler types `unknown` is handed a value nothing
415
+ * has typed yet, which is what a parsed body is; where the source casts
416
+ * that parameter, the cast is what the caller says the body is. The value
417
+ * the chain ends in is what the caller computed from it, and publishing
418
+ * that as the body is the false mismatch this rule exists for.
419
+ *
420
+ * Exactly one such cast across the whole chain answers. None, or more than
421
+ * one (a callback on the failure side can be handed an `unknown` too), and
422
+ * the walk carries on as before. A parameter typed `any` is not read: an
423
+ * unresolved library types every callback that way, success and failure
424
+ * alike, so it says nothing about which one is the body.
425
+ */
426
+ private bodyReadInCallbackChain;
427
+ /**
428
+ * The casts of a callback's first parameter, when the compiler types that
429
+ * parameter `unknown`: `response as T` and `<T>response`, the operand being
430
+ * the parameter itself. A cast that leaves a position open states nothing
431
+ * about the body and is skipped.
432
+ */
433
+ private castsOfUnreadParameter;
364
434
  private bodyReadOnReceiver;
365
435
  private collectDefUseNodes;
366
436
  private expressionUsesNames;
@@ -539,6 +609,8 @@ export declare class TypeInferrer {
539
609
  */
540
610
  private unwrapJsonStringifyArg;
541
611
  private extractExplicitTypeFromAncestor;
612
+ /** The annotation `extractExplicitTypeFromAncestor` prints, as a node. */
613
+ private explicitTypeNodeFromAncestor;
542
614
  /**
543
615
  * Render an explicit annotation (`as T`, `<T>`, or a typed binding) as
544
616
  * fully-structural text.
@@ -20,7 +20,7 @@
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 { isExternalOrigin } from './origin.js';
23
+ import { externalImportsOf, isExternalOrigin } from './origin.js';
24
24
  import { addedDiagnostics, applyInsertions, fileDiagnostics, literalInsertions, mapBack, mapForward, normalise, pathOf, } from './unwidened.js';
25
25
  import { expandTypeStructural, } from './type-structural-expander.js';
26
26
  /**
@@ -277,6 +277,7 @@ export class TypeInferrer {
277
277
  return {
278
278
  program: this.project.getProgram().compilerObject,
279
279
  repoRoot: this.repoRoot,
280
+ imports: externalImportsOf(this.project),
280
281
  };
281
282
  }
282
283
  /**
@@ -940,7 +941,9 @@ export class TypeInferrer {
940
941
  this.log(`Span resolves to a callback-registration call at ${request.file_path}:${request.line_number}; no payload to infer`);
941
942
  return null;
942
943
  }
943
- if (args.length > 0) {
944
+ // A call whose result is a value the repo shapes is not a send: it is
945
+ // the payload (carrick#1732), and drilling would publish its input.
946
+ if (args.length > 0 && !this.callResultIsPayload(node)) {
944
947
  payloadNode = args[0];
945
948
  }
946
949
  }
@@ -973,6 +976,31 @@ export class TypeInferrer {
973
976
  const anchor = this.unwrapArrayLevels(this.unwrapPromiseType(payloadType));
974
977
  return this.createInferredType(request, typeString, false, this.getNodeLocation(payloadNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, this.primaryTypeSymbol(anchor.element), anchor.depth);
975
978
  }
979
+ /**
980
+ * True when a located call's own result is the route's payload, so the
981
+ * transitional drill into its first argument must not run (carrick#1732).
982
+ *
983
+ * `res.json(users)` reached that drill because nothing above it recognised
984
+ * the send: its result reads `void`, `any` or `unknown`, and the payload is
985
+ * the argument. `toPublicView(row)` is the opposite case: a mapper building
986
+ * the object the route sends. Its first argument is the row it was built
987
+ * FROM, which carries columns the route never sends.
988
+ *
989
+ * The call's result decides, not where its callee is declared. It is the
990
+ * payload when it reads as one by the rule a response helper's argument is
991
+ * read with (`nodeCarriesPayloadContract`: object-shaped, not machinery,
992
+ * not `void`/`any`/`unknown`) and the object is not a library's own: a
993
+ * codec's writer from `encode(message)` or a reply builder from a send is
994
+ * the library describing itself, and keeps the drill. A library call that
995
+ * returns the repo's own type (`toInstance(View, plain)`) is the payload.
996
+ */
997
+ callResultIsPayload(call) {
998
+ const { element } = this.unwrapArrayLevels(this.unwrapPromiseType(call.getType()));
999
+ if (this.symbolIsLibOrExternalOrigin(element.getSymbol() ?? element.getAliasSymbol())) {
1000
+ return false;
1001
+ }
1002
+ return this.nodeCarriesPayloadContract(call, false);
1003
+ }
976
1004
  /**
977
1005
  * The wire representation a request's printed type takes. A route response
978
1006
  * is serialised as JSON by every sender this layer reads a payload out of,
@@ -1309,7 +1337,132 @@ export class TypeInferrer {
1309
1337
  }
1310
1338
  }
1311
1339
  }
1312
- return this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(terminalNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, anchor ? this.primaryTypeSymbol(anchor.element) : undefined, anchor?.depth);
1340
+ const inferred = this.createInferredType(request, typeString, isExplicit, this.getNodeLocation(terminalNode), unwrapResult.wasUnwrapped ? unwrapResult.typeString : undefined, anchor ? this.primaryTypeSymbol(anchor.element) : undefined, anchor?.depth);
1341
+ // carrick#1749: say when the text is the source's own statement of the
1342
+ // body, and what that statement is rooted at, so the scanner can tell a
1343
+ // model symbol that names the body from one that names a part of it.
1344
+ const statedBody = explicitType ? this.statedBodyAtRead(terminalNode) : undefined;
1345
+ if (statedBody) {
1346
+ inferred.stated_body = statedBody;
1347
+ }
1348
+ return inferred;
1349
+ }
1350
+ /**
1351
+ * What the source states the body read at `terminal` to be, when the type
1352
+ * `extractExplicitTypeFromAncestor` printed for it is stated AT the read
1353
+ * (carrick#1749): the read is the operand of that cast, or the initializer
1354
+ * of that annotated declaration, through wrappers that leave a value as it
1355
+ * is (parentheses, `await`, `!`, another cast). An annotation further out —
1356
+ * the declared type of the function the read sits in — describes something
1357
+ * else, and is not reported.
1358
+ */
1359
+ statedBodyAtRead(terminal) {
1360
+ const typeNode = this.explicitTypeNodeFromAncestor(terminal);
1361
+ const owner = typeNode?.getParent();
1362
+ if (!typeNode || !owner || this.leavesAPositionOpen(typeNode))
1363
+ return undefined;
1364
+ let current = terminal;
1365
+ for (;;) {
1366
+ if (current === owner)
1367
+ break;
1368
+ const parent = current.getParent();
1369
+ if (!parent)
1370
+ return undefined;
1371
+ if (parent === owner &&
1372
+ Node.isVariableDeclaration(parent) &&
1373
+ parent.getInitializer() === current) {
1374
+ break;
1375
+ }
1376
+ if (!Node.isParenthesizedExpression(parent) &&
1377
+ !Node.isAwaitExpression(parent) &&
1378
+ !Node.isNonNullExpression(parent) &&
1379
+ !Node.isAsExpression(parent) &&
1380
+ !Node.isTypeAssertion(parent) &&
1381
+ !Node.isSatisfiesExpression(parent)) {
1382
+ return undefined;
1383
+ }
1384
+ current = parent;
1385
+ }
1386
+ return this.statedRoot(typeNode);
1387
+ }
1388
+ /**
1389
+ * A stated type with `any` or `unknown` written anywhere in it (`unknown`,
1390
+ * `Record<string, unknown>`, `{ items: any[] }`) leaves a position open: it
1391
+ * is a placeholder the source narrows later (`const data: unknown = await
1392
+ * res.json()`, then `data as Entry[]`), not its statement of the body.
1393
+ */
1394
+ leavesAPositionOpen(typeNode) {
1395
+ const open = (node) => node.getKind() === SyntaxKind.AnyKeyword || node.getKind() === SyntaxKind.UnknownKeyword;
1396
+ return open(typeNode) || typeNode.getDescendants().some(open);
1397
+ }
1398
+ /**
1399
+ * The named root of a stated body type: `Promise<...>` and `PromiseLike`
1400
+ * (the language's await protocol), array levels, parentheses, `readonly`
1401
+ * and `| null`/`| undefined` are peeled, and what is left is either a type
1402
+ * reference, whose name is the root, or anything else, which has none.
1403
+ */
1404
+ statedRoot(typeNode) {
1405
+ let node = typeNode;
1406
+ let depth = 0;
1407
+ for (let step = 0; step < 16; step++) {
1408
+ if (Node.isParenthesizedTypeNode(node)) {
1409
+ node = node.getTypeNode();
1410
+ continue;
1411
+ }
1412
+ if (Node.isArrayTypeNode(node)) {
1413
+ node = node.getElementTypeNode();
1414
+ depth++;
1415
+ continue;
1416
+ }
1417
+ if (Node.isTypeOperatorTypeNode(node) &&
1418
+ node.getOperator() === SyntaxKind.ReadonlyKeyword) {
1419
+ node = node.getTypeNode();
1420
+ continue;
1421
+ }
1422
+ if (Node.isUnionTypeNode(node)) {
1423
+ const present = node
1424
+ .getTypeNodes()
1425
+ .filter((member) => !((Node.isLiteralTypeNode(member) &&
1426
+ member.getLiteral().getKind() === SyntaxKind.NullKeyword) ||
1427
+ member.getKind() === SyntaxKind.UndefinedKeyword ||
1428
+ member.getKind() === SyntaxKind.NullKeyword));
1429
+ if (present.length === 1) {
1430
+ node = present[0];
1431
+ continue;
1432
+ }
1433
+ break;
1434
+ }
1435
+ if (Node.isTypeReference(node)) {
1436
+ const name = node.getTypeName().getText();
1437
+ const args = node.getTypeArguments();
1438
+ if (args.length === 1 && (name === 'Promise' || name === 'PromiseLike')) {
1439
+ node = args[0];
1440
+ continue;
1441
+ }
1442
+ if (args.length === 1 && (name === 'Array' || name === 'ReadonlyArray')) {
1443
+ node = args[0];
1444
+ depth++;
1445
+ continue;
1446
+ }
1447
+ }
1448
+ break;
1449
+ }
1450
+ const depthField = depth > 0 ? { array_depth: depth } : {};
1451
+ if (!Node.isTypeReference(node)) {
1452
+ return depthField;
1453
+ }
1454
+ const typeName = node.getTypeName();
1455
+ const nameNode = Node.isQualifiedName(typeName) ? typeName.getRight() : typeName;
1456
+ let symbol = nameNode.getSymbol();
1457
+ if (symbol?.isAlias()) {
1458
+ symbol = symbol.getAliasedSymbol() ?? symbol;
1459
+ }
1460
+ const source = symbol?.getDeclarations()[0]?.getSourceFile().getFilePath();
1461
+ return {
1462
+ root: nameNode.getText(),
1463
+ ...(source ? { root_source: source } : {}),
1464
+ ...depthField,
1465
+ };
1313
1466
  }
1314
1467
  inferVariable(sourceFile, request, extractionConfig) {
1315
1468
  const node = this.resolveTargetNode(sourceFile, request);
@@ -1632,6 +1785,14 @@ export class TypeInferrer {
1632
1785
  // Call Result Resolution
1633
1786
  // ===========================================================================
1634
1787
  resolveCallResultTerminalNode(callExpr, func) {
1788
+ // carrick#1749: the call starts a chain, and a callback in it is handed
1789
+ // the body unread and casts it. That cast is the body read, and it comes
1790
+ // before the return-statement answer below, which for a chain is the
1791
+ // value the caller computed from the body, not the body.
1792
+ const chainRead = this.bodyReadInCallbackChain(callExpr);
1793
+ if (chainRead) {
1794
+ return { terminal: chainRead, projectionOnly: false, projections: [] };
1795
+ }
1635
1796
  const returnStmt = callExpr.getFirstAncestorByKind(SyntaxKind.ReturnStatement);
1636
1797
  if (returnStmt) {
1637
1798
  const returnExpr = returnStmt.getExpression();
@@ -2094,6 +2255,80 @@ export class TypeInferrer {
2094
2255
  * HTTP response is read exactly this way whatever produced the response, so
2095
2256
  * the shape is structural, not a framework's name.
2096
2257
  */
2258
+ /**
2259
+ * The body read inside a chain that starts at `callExpr` (carrick#1749):
2260
+ * `request(url).check(ok).mapOk(response => { const data = response as
2261
+ * SearchResponse; ... })`.
2262
+ *
2263
+ * The chain is the run of member calls whose receiver is the call, then
2264
+ * that call's result, and so on. A callback passed to one of them whose
2265
+ * first parameter the compiler types `unknown` is handed a value nothing
2266
+ * has typed yet, which is what a parsed body is; where the source casts
2267
+ * that parameter, the cast is what the caller says the body is. The value
2268
+ * the chain ends in is what the caller computed from it, and publishing
2269
+ * that as the body is the false mismatch this rule exists for.
2270
+ *
2271
+ * Exactly one such cast across the whole chain answers. None, or more than
2272
+ * one (a callback on the failure side can be handed an `unknown` too), and
2273
+ * the walk carries on as before. A parameter typed `any` is not read: an
2274
+ * unresolved library types every callback that way, success and failure
2275
+ * alike, so it says nothing about which one is the body.
2276
+ */
2277
+ bodyReadInCallbackChain(callExpr) {
2278
+ const reads = [];
2279
+ let receiver = callExpr;
2280
+ for (let link = 0; link < 64; link++) {
2281
+ const access = receiver.getParent();
2282
+ if (!access ||
2283
+ !Node.isPropertyAccessExpression(access) ||
2284
+ access.getExpression() !== receiver) {
2285
+ break;
2286
+ }
2287
+ const call = access.getParent();
2288
+ if (!call || !Node.isCallExpression(call) || call.getExpression() !== access) {
2289
+ break;
2290
+ }
2291
+ for (const arg of call.getArguments()) {
2292
+ if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {
2293
+ reads.push(...this.castsOfUnreadParameter(arg));
2294
+ }
2295
+ }
2296
+ receiver = call;
2297
+ }
2298
+ return reads.length === 1 ? reads[0] : undefined;
2299
+ }
2300
+ /**
2301
+ * The casts of a callback's first parameter, when the compiler types that
2302
+ * parameter `unknown`: `response as T` and `<T>response`, the operand being
2303
+ * the parameter itself. A cast that leaves a position open states nothing
2304
+ * about the body and is skipped.
2305
+ */
2306
+ castsOfUnreadParameter(callback) {
2307
+ const param = callback.getParameters()[0];
2308
+ const name = param?.getNameNode();
2309
+ if (!param || !name || !Node.isIdentifier(name) || !param.getType().isUnknown()) {
2310
+ return [];
2311
+ }
2312
+ const symbol = name.getSymbol();
2313
+ if (!symbol)
2314
+ return [];
2315
+ return callback.getDescendants().filter((node) => {
2316
+ if (!Node.isAsExpression(node) && !Node.isTypeAssertion(node))
2317
+ return false;
2318
+ let operand = node.getExpression();
2319
+ while (Node.isParenthesizedExpression(operand)) {
2320
+ operand = operand.getExpression();
2321
+ }
2322
+ if (!Node.isIdentifier(operand) || operand.getSymbol() !== symbol)
2323
+ return false;
2324
+ const stated = node.getType();
2325
+ const statedNode = node.getTypeNode();
2326
+ return (!!statedNode &&
2327
+ !stated.isAny() &&
2328
+ !stated.isUnknown() &&
2329
+ !this.leavesAPositionOpen(statedNode));
2330
+ });
2331
+ }
2097
2332
  bodyReadOnReceiver(identifier) {
2098
2333
  const access = identifier.getParent();
2099
2334
  if (!access ||
@@ -2610,7 +2845,7 @@ export class TypeInferrer {
2610
2845
  }
2611
2846
  const program = this.project.getProgram().compilerObject;
2612
2847
  for (const decl of symbol.getDeclarations()) {
2613
- if (isExternalOrigin(program, decl.getSourceFile().compilerNode, this.repoRoot)) {
2848
+ if (isExternalOrigin(program, decl.getSourceFile().compilerNode, this.repoRoot, externalImportsOf(this.project))) {
2614
2849
  return true;
2615
2850
  }
2616
2851
  }
@@ -2796,11 +3031,16 @@ export class TypeInferrer {
2796
3031
  return this.unwrapExpressionNode(args[0]);
2797
3032
  }
2798
3033
  extractExplicitTypeFromAncestor(node) {
3034
+ const typeNode = this.explicitTypeNodeFromAncestor(node);
3035
+ return typeNode ? this.expandAnnotationTypeNode(typeNode) : null;
3036
+ }
3037
+ /** The annotation `extractExplicitTypeFromAncestor` prints, as a node. */
3038
+ explicitTypeNodeFromAncestor(node) {
2799
3039
  const varDecl = node.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
2800
3040
  if (varDecl) {
2801
3041
  const typeNode = varDecl.getTypeNode();
2802
3042
  if (typeNode) {
2803
- return this.expandAnnotationTypeNode(typeNode);
3043
+ return typeNode;
2804
3044
  }
2805
3045
  }
2806
3046
  // Consider the node ITSELF as well as its ancestors: the `call_result`
@@ -2813,7 +3053,7 @@ export class TypeInferrer {
2813
3053
  if (asExpr) {
2814
3054
  const typeNode = asExpr.getTypeNode();
2815
3055
  if (typeNode) {
2816
- return this.expandAnnotationTypeNode(typeNode);
3056
+ return typeNode;
2817
3057
  }
2818
3058
  }
2819
3059
  const typeAssertion = Node.isTypeAssertion(node)
@@ -2822,10 +3062,10 @@ export class TypeInferrer {
2822
3062
  if (typeAssertion) {
2823
3063
  const typeNode = typeAssertion.getTypeNode();
2824
3064
  if (typeNode) {
2825
- return this.expandAnnotationTypeNode(typeNode);
3065
+ return typeNode;
2826
3066
  }
2827
3067
  }
2828
- return null;
3068
+ return undefined;
2829
3069
  }
2830
3070
  /**
2831
3071
  * Render an explicit annotation (`as T`, `<T>`, or a typed binding) as
@@ -29,6 +29,7 @@
29
29
  * structural form rather than a dangling name.
30
30
  */
31
31
  import { type Node, type Type, ts } from 'ts-morph';
32
+ import { type ExternalImports } from './origin.js';
32
33
  /**
33
34
  * Bound on the structural-expansion recursion. Deep enough for every realistic
34
35
  * request/response shape; a backstop against pathological/recursive types the
@@ -83,11 +84,14 @@ export type WireFormat = 'declared' | 'json';
83
84
  * its own cache leaves no such segment in the path (carrick#1264). The program
84
85
  * carries the resolver's own verdict, so it is what `isExternalOrigin` is
85
86
  * asked — the same instrument the inference path uses, so the two layers
86
- * cannot disagree about which types to inline.
87
+ * cannot disagree about which types to inline. `imports` is that verdict kept
88
+ * past the program rebuilds that drop it (carrick#1731), for a project the
89
+ * loader built.
87
90
  */
88
91
  export interface ExpandOrigin {
89
92
  readonly program: ts.Program;
90
93
  readonly repoRoot: string;
94
+ readonly imports?: ExternalImports;
91
95
  }
92
96
  /** Everything `expandTypeStructural` takes besides the type and its origin. */
93
97
  export interface ExpandOptions {
@@ -366,7 +366,7 @@ function isLibraryType(type, origin) {
366
366
  const decls = symbol.getDeclarations();
367
367
  if (decls.length === 0)
368
368
  return false;
369
- return decls.some((decl) => isExternalOrigin(origin.program, decl.getSourceFile().compilerNode, origin.repoRoot));
369
+ return decls.some((decl) => isExternalOrigin(origin.program, decl.getSourceFile().compilerNode, origin.repoRoot, origin.imports));
370
370
  }
371
371
  /**
372
372
  * Non-expanded text for a type. Passes `undefined` as the enclosing node so