carrick 0.3.133 → 0.3.134

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 (64) hide show
  1. package/dist/contract.d.ts +6 -0
  2. package/dist/contract.js.map +1 -1
  3. package/dist/hook/post-edit.d.ts +10 -5
  4. package/dist/hook/post-edit.js +80 -23
  5. package/dist/hook/post-edit.js.map +1 -1
  6. package/dist/hook/refresh.d.ts +23 -3
  7. package/dist/hook/refresh.js +48 -6
  8. package/dist/hook/refresh.js.map +1 -1
  9. package/dist/hook/reuse.d.ts +7 -21
  10. package/dist/hook/reuse.js +23 -36
  11. package/dist/hook/reuse.js.map +1 -1
  12. package/dist/hook/session-start.js +15 -2
  13. package/dist/hook/session-start.js.map +1 -1
  14. package/dist/hook/stop.js.map +1 -1
  15. package/dist/init/agents-section.js +1 -1
  16. package/dist/init/agents-section.js.map +1 -1
  17. package/dist/init/doctor.js +4 -0
  18. package/dist/init/doctor.js.map +1 -1
  19. package/dist/init/hosted.d.ts +1 -0
  20. package/dist/init/hosted.js +4 -1
  21. package/dist/init/hosted.js.map +1 -1
  22. package/dist/init/remove.js +7 -1
  23. package/dist/init/remove.js.map +1 -1
  24. package/dist/init/repo-copies.js +3 -1
  25. package/dist/init/repo-copies.js.map +1 -1
  26. package/dist/init/run.d.ts +1 -1
  27. package/dist/init/run.js +10 -2
  28. package/dist/init/run.js.map +1 -1
  29. package/dist/init/task-skills.d.ts +22 -1
  30. package/dist/init/task-skills.js +65 -1
  31. package/dist/init/task-skills.js.map +1 -1
  32. package/dist/render.d.ts +41 -16
  33. package/dist/render.js +91 -102
  34. package/dist/render.js.map +1 -1
  35. package/package.json +10 -9
  36. package/plugin/.claude-plugin/plugin.json +1 -1
  37. package/sidecar/dist/src/capture/anchors.js +18 -1
  38. package/sidecar/dist/src/capture/api.d.ts +17 -4
  39. package/sidecar/dist/src/capture/check-workspace.js +1 -1
  40. package/sidecar/dist/src/capture/check.d.ts +44 -0
  41. package/sidecar/dist/src/capture/check.js +112 -10
  42. package/sidecar/dist/src/capture/installed-package.d.ts +5 -0
  43. package/sidecar/dist/src/capture/installed-package.js +64 -6
  44. package/sidecar/dist/src/capture/paths-rewrite.js +3 -3
  45. package/sidecar/dist/src/capture/result-label.d.ts +42 -0
  46. package/sidecar/dist/src/capture/result-label.js +91 -0
  47. package/sidecar/dist/src/capture/self-check.js +13 -2
  48. package/sidecar/dist/src/index.js +116 -4
  49. package/sidecar/dist/src/infer-cache.d.ts +125 -0
  50. package/sidecar/dist/src/infer-cache.js +244 -0
  51. package/sidecar/dist/src/library-claims.js +42 -4
  52. package/sidecar/dist/src/node-index.d.ts +53 -0
  53. package/sidecar/dist/src/node-index.js +100 -0
  54. package/sidecar/dist/src/project-loader.d.ts +11 -0
  55. package/sidecar/dist/src/project-loader.js +15 -0
  56. package/sidecar/dist/src/result-candidates.d.ts +28 -0
  57. package/sidecar/dist/src/result-candidates.js +209 -0
  58. package/sidecar/dist/src/type-inferrer.d.ts +27 -0
  59. package/sidecar/dist/src/type-inferrer.js +175 -43
  60. package/sidecar/dist/src/types.d.ts +52 -0
  61. package/sidecar/dist/src/unwidened.js +10 -1
  62. package/sidecar/dist/src/validators.d.ts +919 -197
  63. package/sidecar/dist/src/validators.js +32 -0
  64. package/templates/skills/carrick-reuse.md +11 -0
@@ -61,6 +61,31 @@ export function carriesHeapLimitLine(stderr) {
61
61
  .some((line) => line.includes(HEAP_OUT_OF_MEMORY) &&
62
62
  !HARD_SIZE_LIMITS.some((limit) => line.includes(limit)));
63
63
  }
64
+ /**
65
+ * Whether the native compiler (TypeScript 7, a Go program) died out of
66
+ * memory. The Go runtime prints `fatal error: runtime: out of memory` and
67
+ * exits 2; V8's line above never appears, and a heap cap passed through
68
+ * `NODE_OPTIONS` reaches only the launcher, not the compiler.
69
+ */
70
+ export function carriesNativeOutOfMemoryLine(output) {
71
+ return output.split('\n').some((line) => line.includes('fatal error: runtime: out of memory'));
72
+ }
73
+ /**
74
+ * Whether a diagnostic is about the compiler's configuration rather than a
75
+ * file it checked: one placed in the tsconfig the judge wrote, or "no inputs"
76
+ * (TS18003). Keyed on the file, not on the TS5xxx option range: some of those
77
+ * codes sit in a source file (TS5097, an import ending in `.ts`) and poison
78
+ * only the stub that holds it.
79
+ */
80
+ export function isConfigDiagnostic(d) {
81
+ return path.posix.basename(d.file) === 'tsconfig.json' || d.code === 18003;
82
+ }
83
+ /** Why the compiler ended abnormally, when its output names a known cause. */
84
+ export function abnormalCause(output) {
85
+ if (carriesNativeOutOfMemoryLine(output))
86
+ return 'the type checker ran out of memory';
87
+ return undefined;
88
+ }
64
89
  /** V8's default old-space ceiling on a 64-bit host, in MB. */
65
90
  const V8_DEFAULT_HEAP_CEILING_MB = 4096;
66
91
  /**
@@ -119,9 +144,16 @@ function sidecarRoot() {
119
144
  * which is the caller's "not found" signal and the most useful thing to name.
120
145
  */
121
146
  export function resolveVendoredBin(name, from = sidecarRoot()) {
122
- const local = path.join(from, 'node_modules', '.bin', name);
147
+ return resolveVendored(path.join('.bin', name), from);
148
+ }
149
+ /**
150
+ * The first `node_modules/<rel>` at or above `from`, the same walk as
151
+ * {@link resolveVendoredBin}; the sidecar-local path when none exists.
152
+ */
153
+ export function resolveVendored(rel, from = sidecarRoot()) {
154
+ const local = path.join(from, 'node_modules', rel);
123
155
  for (let dir = from;; dir = path.dirname(dir)) {
124
- const candidate = path.join(dir, 'node_modules', '.bin', name);
156
+ const candidate = path.join(dir, 'node_modules', rel);
125
157
  if (fs.existsSync(candidate))
126
158
  return candidate;
127
159
  if (path.dirname(dir) === dir)
@@ -131,6 +163,58 @@ export function resolveVendoredBin(name, from = sidecarRoot()) {
131
163
  function binPath(name) {
132
164
  return resolveVendoredBin(name);
133
165
  }
166
+ /**
167
+ * The package that brings the judge's compiler: TypeScript 7 under an alias
168
+ * beside the TypeScript 6 that capture and inference read through ts-morph.
169
+ * Its platform package holds the native executable the judge runs. Both
170
+ * packages declare a `tsc` bin, so `node_modules/.bin/tsc` may be either; the
171
+ * judge never reads it.
172
+ */
173
+ export const JUDGE_COMPILER_PACKAGE = 'typescript-native';
174
+ /**
175
+ * The judge's tsc: the host's native executable, from the platform package
176
+ * the compiler installs as an optional dependency (`@typescript/
177
+ * typescript-<platform>-<arch>`), found by the same walk as the bins. Where it
178
+ * is not installed (an install that omits optional dependencies, such as the
179
+ * cloud runner's, or a pnpm store the walk does not reach), TypeScript 6's own
180
+ * tsc, which ships for ts-morph anyway. The two read every probe the same
181
+ * (measured on 1,018 real probes; the one wording difference moves only a
182
+ * verdict's `codes`), so the fallback changes no verdict, only the time.
183
+ */
184
+ export function judgeTscPath(from = sidecarRoot(), platform = process.platform, arch = process.arch) {
185
+ const exe = platform === 'win32' ? 'tsc.exe' : 'tsc';
186
+ const native = path.join('@typescript', `typescript-${platform}-${arch}`, 'lib', exe);
187
+ const found = resolveVendored(native, from);
188
+ if (fs.existsSync(found))
189
+ return found;
190
+ return resolveVendored(path.join('typescript', 'bin', 'tsc'), from);
191
+ }
192
+ /** Which compiler a tsc path is, in words, for the run log and the result. */
193
+ export function judgeLabel(tscPath) {
194
+ const version = compilerVersion(tscPath);
195
+ const parts = tscPath.split(/[\\/]/);
196
+ if (parts.some((p) => p.startsWith('typescript-') && p !== JUDGE_COMPILER_PACKAGE)) {
197
+ return `TypeScript ${version} (native)`;
198
+ }
199
+ if (parts.includes('typescript') && parts[parts.length - 2] === 'bin') {
200
+ return `TypeScript ${version} (native compiler not installed)`;
201
+ }
202
+ return `a stand-in compiler at ${tscPath}`;
203
+ }
204
+ /** The version of the compiler a tsc path belongs to, read from the package
205
+ * that holds it; the compiler-API version for a stand-in with no package. */
206
+ function compilerVersion(tscPath) {
207
+ try {
208
+ const manifest = JSON.parse(fs.readFileSync(path.join(path.dirname(tscPath), '..', 'package.json'), 'utf8'));
209
+ const compiler = manifest.name === 'typescript' || manifest.name?.startsWith('@typescript/typescript-');
210
+ if (compiler && manifest.version)
211
+ return manifest.version;
212
+ }
213
+ catch {
214
+ // A stand-in compiler (tests) sits in no package.
215
+ }
216
+ return ts.version;
217
+ }
134
218
  function unverifiableAll(plans, gate, diagnostic) {
135
219
  return plans.map((plan) => ({
136
220
  pair_id: plan.pairId,
@@ -155,8 +239,9 @@ export async function runCheck(opts, onProgress) {
155
239
  const progress = onProgress ?? (() => { });
156
240
  const cleanup = opts.cleanup !== false;
157
241
  const pnpmPath = opts.pnpmPath ?? binPath('pnpm');
158
- const tscPath = opts.tscPath ?? binPath('tsc');
159
- const tsVersion = ts.version;
242
+ const tscPath = opts.tscPath ?? judgeTscPath();
243
+ const tsVersion = compilerVersion(tscPath);
244
+ const judge = judgeLabel(tscPath);
160
245
  // Isolation is non-negotiable: without the vendored pnpm we would fall back
161
246
  // to a flat npm install that manufactures nominal false-incompatibles. Fail
162
247
  // explicitly instead (design Check step 2).
@@ -169,6 +254,7 @@ export async function runCheck(opts, onProgress) {
169
254
  isolation: 'unavailable',
170
255
  install_ok: false,
171
256
  ts_version: tsVersion,
257
+ judge,
172
258
  verdicts: sortVerdicts([
173
259
  ...unverifiableAll(probing, 'isolation:unavailable', 'type isolation unavailable (pnpm missing); compatibility cannot be verified.'),
174
260
  ...preGated,
@@ -248,13 +334,14 @@ export async function runCheck(opts, onProgress) {
248
334
  isolation: 'pnpm',
249
335
  install_ok: false,
250
336
  ts_version: tsVersion,
337
+ judge,
251
338
  verdicts,
252
339
  degraded_services: degraded,
253
340
  errors,
254
341
  };
255
342
  }
256
343
  // ---- Judge (async, off the event loop) ----------------------------------
257
- progress('checking', 'type-checking probes');
344
+ progress('checking', `type-checking probes (judge: ${judge})`);
258
345
  const heap = childHeap(process.execArgv, process.env.NODE_OPTIONS, process.memoryUsage().rss / (1024 * 1024));
259
346
  const tsc = await runProcess(tscPath, ['--noEmit', '--pretty', 'false', '-p', path.join(ws.probesRel, 'tsconfig.json')], ws.workspaceDir, { ...process.env, NODE_OPTIONS: heap.nodeOptions });
260
347
  const diagnostics = parseTscOutput(tsc.stdout + '\n' + tsc.stderr);
@@ -267,8 +354,20 @@ export async function runCheck(opts, onProgress) {
267
354
  // `code === 0` guard.
268
355
  // A compiler a signal ended never finished, so a pair with no diagnostic
269
356
  // was never cleared, whatever it printed first (carrick#2209).
270
- if (tsc.signal !== null || (tsc.code !== 0 && diagnostics.length === 0)) {
271
- const excerpt = scrubPaths((tsc.stderr || tsc.stdout).trim().slice(0, 2000), scrubCtx);
357
+ //
358
+ // A diagnostic about the configuration (a tsconfig the compiler refuses,
359
+ // an option it does not know) is printed WITH a position, in the tsconfig,
360
+ // and belongs to no pair: TypeScript 7 refuses an option and checks
361
+ // nothing, so without this every pair read compatible. Such a run checked
362
+ // no pair, whatever else it printed.
363
+ const configError = diagnostics.find(isConfigDiagnostic);
364
+ if (tsc.signal !== null ||
365
+ (tsc.code !== 0 && diagnostics.length === 0) ||
366
+ configError !== undefined) {
367
+ const excerpt = scrubPaths((configError
368
+ ? `${configError.file}(${configError.line},${configError.col}): error TS${configError.code}: ${configError.message}`
369
+ : (tsc.stderr || tsc.stdout).trim()).slice(0, 2000), scrubCtx);
370
+ const cause = abnormalCause(tsc.stderr + '\n' + tsc.stdout);
272
371
  errors.push(`tsc terminated abnormally (exit code ${tsc.code ?? 'null'}) ` +
273
372
  `with no parseable diagnostics${excerpt ? `: ${excerpt}` : ''}`);
274
373
  for (const s of opts.stubs) {
@@ -278,8 +377,9 @@ export async function runCheck(opts, onProgress) {
278
377
  });
279
378
  }
280
379
  const verdicts = sortVerdicts([
281
- ...unverifiableAll(probing, 'tsc:abnormal-termination', `the type checker terminated abnormally (exit code ${tsc.code ?? 'null'}); ` +
282
- 'compatibility cannot be verified.' +
380
+ ...unverifiableAll(probing, 'tsc:abnormal-termination', `the type checker terminated abnormally (exit code ${tsc.code ?? 'null'})` +
381
+ (cause ? ` because ${cause}` : '') +
382
+ '; compatibility cannot be verified.' +
283
383
  (excerpt ? ` Compiler output: ${excerpt}` : '')),
284
384
  ...preGated,
285
385
  ...unresolved,
@@ -290,7 +390,7 @@ export async function runCheck(opts, onProgress) {
290
390
  const compilerEnd = {
291
391
  exit_code: tsc.code,
292
392
  signal: tsc.signal,
293
- heap_limit: carriesHeapLimitLine(tsc.stderr),
393
+ heap_limit: carriesHeapLimitLine(tsc.stderr) || carriesNativeOutOfMemoryLine(tsc.stderr + '\n' + tsc.stdout),
294
394
  heap_cap_mb: heap.heapCapMb,
295
395
  };
296
396
  if (cleanup)
@@ -301,6 +401,7 @@ export async function runCheck(opts, onProgress) {
301
401
  isolation: 'pnpm',
302
402
  install_ok: true,
303
403
  ts_version: tsVersion,
404
+ judge,
304
405
  verdicts,
305
406
  degraded_services: dedupeDegraded(degraded),
306
407
  errors,
@@ -359,6 +460,7 @@ export async function runCheck(opts, onProgress) {
359
460
  isolation: 'pnpm',
360
461
  install_ok: true,
361
462
  ts_version: tsVersion,
463
+ judge,
362
464
  verdicts,
363
465
  degraded_services: dedupeDegraded(degraded),
364
466
  errors,
@@ -35,6 +35,11 @@ export interface InstalledPackageSpecifier {
35
35
  name: string;
36
36
  version: string;
37
37
  };
38
+ /**
39
+ * The name the entry exports the declaration under, set only when it differs
40
+ * from the name the path's declaration carries (`export { O as OmitKeys }`).
41
+ */
42
+ exportName?: string;
38
43
  }
39
44
  /**
40
45
  * The `@types/*` package that serves a bare name's declarations, for callers
@@ -245,29 +245,78 @@ function entrySubpaths(manifest) {
245
245
  }
246
246
  return [''];
247
247
  }
248
+ /**
249
+ * Declaration keys (`file\0declaredName`) of what `file` exports as `name`.
250
+ * A chunk can declare a type under its public name and export it under a
251
+ * short one (`export { type OmitKeyof as O }`), so the name read off the
252
+ * import is not the name the declaration carries.
253
+ */
254
+ function declarationKeysOfExport(file, name) {
255
+ const program = ts.createProgram([file], RESOLUTION_OPTIONS);
256
+ const checker = program.getTypeChecker();
257
+ const source = program.getSourceFile(file);
258
+ const moduleSymbol = source && checker.getSymbolAtLocation(source);
259
+ const symbol = moduleSymbol && checker.getExportsOfModule(moduleSymbol).find((s) => s.getName() === name);
260
+ if (!symbol)
261
+ return [];
262
+ let target = symbol;
263
+ for (let hops = 0; target.flags & ts.SymbolFlags.Alias && hops < 16; hops++) {
264
+ const next = checker.getAliasedSymbol(target);
265
+ if (next === target)
266
+ break;
267
+ target = next;
268
+ }
269
+ const keys = [];
270
+ for (const declaration of target.declarations ?? []) {
271
+ try {
272
+ keys.push(`${fs.realpathSync(declaration.getSourceFile().fileName)}\0${target.getName()}`);
273
+ }
274
+ catch {
275
+ // declaration outside the real file system: ignore
276
+ }
277
+ }
278
+ return keys;
279
+ }
248
280
  const moduleExportsCache = new Map();
249
- /** Exported names of `entryFile`, each with the real files declaring it. */
281
+ /**
282
+ * Exports of `entryFile`: by exported name (with the real files declaring it)
283
+ * and by declaration identity (the file and the name the declaration carries
284
+ * there, whatever name the entry exports it under).
285
+ */
250
286
  function exportsOf(entryFile) {
251
287
  const cached = moduleExportsCache.get(entryFile);
252
288
  if (cached)
253
289
  return cached;
254
- const exported = new Map();
290
+ const exported = { byName: new Map(), byDeclaration: new Map() };
255
291
  const program = ts.createProgram([entryFile], RESOLUTION_OPTIONS);
256
292
  const checker = program.getTypeChecker();
257
293
  const source = program.getSourceFile(entryFile);
258
294
  const moduleSymbol = source && checker.getSymbolAtLocation(source);
259
295
  for (const symbol of moduleSymbol ? checker.getExportsOfModule(moduleSymbol) : []) {
260
- const target = symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
296
+ let target = symbol;
297
+ // Follow the alias chain until it stops (an entry can re-export a re-export).
298
+ for (let hops = 0; target.flags & ts.SymbolFlags.Alias && hops < 16; hops++) {
299
+ const next = checker.getAliasedSymbol(target);
300
+ if (next === target)
301
+ break;
302
+ target = next;
303
+ }
261
304
  const files = new Set();
262
305
  for (const declaration of target.declarations ?? []) {
263
306
  try {
264
- files.add(fs.realpathSync(declaration.getSourceFile().fileName));
307
+ const real = fs.realpathSync(declaration.getSourceFile().fileName);
308
+ files.add(real);
309
+ const key = `${real}\0${target.getName()}`;
310
+ // The first export of a declaration wins; its own name wins over a rename.
311
+ if (!exported.byDeclaration.has(key) || symbol.getName() === target.getName()) {
312
+ exported.byDeclaration.set(key, symbol.getName());
313
+ }
265
314
  }
266
315
  catch {
267
316
  // declaration outside the real file system: ignore
268
317
  }
269
318
  }
270
- exported.set(symbol.getName(), files);
319
+ exported.byName.set(symbol.getName(), files);
271
320
  }
272
321
  moduleExportsCache.set(entryFile, exported);
273
322
  return exported;
@@ -304,9 +353,18 @@ export function installedPackageSpecifier(absoluteSpecifier, importedName) {
304
353
  const entryFile = resolveAgainstInstall(publicName + entry, root, manifest.name)?.resolvedFileName;
305
354
  if (!entryFile)
306
355
  continue;
307
- if (exportsOf(entryFile).get(importedName)?.has(file)) {
356
+ const entryExports = exportsOf(entryFile);
357
+ if (entryExports.byName.get(importedName)?.has(file)) {
308
358
  return withPin(publicName + entry);
309
359
  }
360
+ // The entry may export the declaration under another name.
361
+ const renamed = entryExports.byDeclaration.get(`${file}\0${importedName}`) ??
362
+ declarationKeysOfExport(file, importedName)
363
+ .map((key) => entryExports.byDeclaration.get(key))
364
+ .find((name) => name !== undefined);
365
+ if (renamed !== undefined) {
366
+ return { ...withPin(publicName + entry), exportName: renamed };
367
+ }
310
368
  }
311
369
  }
312
370
  return withPin(publicName + (candidates[0] ?? `/${withoutExtension(subpath)}`));
@@ -82,7 +82,7 @@ export function rewriteEmittedSpecifiers(args) {
82
82
  installs[mapped.install.name] = mapped.install.root;
83
83
  if (mapped.pin)
84
84
  pins[mapped.pin.name] = mapped.pin.version;
85
- return mapped.specifier;
85
+ return { specifier: mapped.specifier, exportName: mapped.exportName };
86
86
  };
87
87
  for (const file of args.files) {
88
88
  const absFile = path.join(args.typesDir, file);
@@ -97,13 +97,13 @@ export function rewriteEmittedSpecifiers(args) {
97
97
  if (replacement === undefined)
98
98
  return whole;
99
99
  importTypeRewrites++;
100
- return `${open}${quote}${replacement}${quote}${close}${dot}${name}`;
100
+ return `${open}${quote}${replacement.specifier}${quote}${close}${dot}${replacement.exportName ?? name}`;
101
101
  });
102
102
  const { text: rewritten, rewrites } = rewriteSpecifiers(text, (spec) => {
103
103
  // Absolute paths: an emitted tree file, else an installed package.
104
104
  if (spec.startsWith('/')) {
105
105
  const target = treeFileFor(spec, args.entryDir, emitted, args.outside);
106
- return target ? relativeSpecifier(file, target) : installed(spec);
106
+ return target ? relativeSpecifier(file, target) : installed(spec)?.specifier;
107
107
  }
108
108
  if (isRelative(spec))
109
109
  return undefined;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Reading a labelled call's result (carrick#2818).
3
+ *
4
+ * A request library's call rarely returns the response body itself: the body
5
+ * sits in a member of what comes back (`data`), behind a parse method
6
+ * (`json()`), or is the whole value. The library store labels which, per
7
+ * operation, and the scanner sends the label on the consumer's infer anchor.
8
+ * This reads the labelled part of the located call's type, so the contract the
9
+ * consumer states is the body and never the library's envelope.
10
+ *
11
+ * The label is one of:
12
+ * - `{ whole: true }`: the awaited type;
13
+ * - `{ member: [m] }`: `m` on every non-nullish constituent of the awaited
14
+ * type, `undefined` and `null` removed from what is read;
15
+ * - `{ parse: p }`: the awaited return of calling `p()`, read off the type as
16
+ * the call returns it (a promise-like with a `json()` method is a thenable,
17
+ * and awaiting it first would drop the method).
18
+ *
19
+ * A label the type cannot satisfy abstains with `result_member_missing`.
20
+ */
21
+ import ts from 'typescript';
22
+ /** The wire form of a result label. Exactly one key is set. */
23
+ export type ResultLabel = {
24
+ whole: true;
25
+ } | {
26
+ member: [string];
27
+ } | {
28
+ parse: string;
29
+ };
30
+ export type ResultRead = {
31
+ type: ts.Type;
32
+ } | {
33
+ abstain: string;
34
+ };
35
+ /** The constituents of a union, or the type alone. */
36
+ export declare function constituentsOf(type: ts.Type): readonly ts.Type[];
37
+ /**
38
+ * Read `label` off a call's type. `raw` is the type as the call returns it and
39
+ * `awaited` the type after the transport unwrap; `at` is the node properties
40
+ * are instantiated at.
41
+ */
42
+ export declare function readResultLabel(checker: ts.TypeChecker, raw: ts.Type, awaited: ts.Type, label: ResultLabel, at: ts.Node): ResultRead;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Reading a labelled call's result (carrick#2818).
3
+ *
4
+ * A request library's call rarely returns the response body itself: the body
5
+ * sits in a member of what comes back (`data`), behind a parse method
6
+ * (`json()`), or is the whole value. The library store labels which, per
7
+ * operation, and the scanner sends the label on the consumer's infer anchor.
8
+ * This reads the labelled part of the located call's type, so the contract the
9
+ * consumer states is the body and never the library's envelope.
10
+ *
11
+ * The label is one of:
12
+ * - `{ whole: true }`: the awaited type;
13
+ * - `{ member: [m] }`: `m` on every non-nullish constituent of the awaited
14
+ * type, `undefined` and `null` removed from what is read;
15
+ * - `{ parse: p }`: the awaited return of calling `p()`, read off the type as
16
+ * the call returns it (a promise-like with a `json()` method is a thenable,
17
+ * and awaiting it first would drop the method).
18
+ *
19
+ * A label the type cannot satisfy abstains with `result_member_missing`.
20
+ */
21
+ import ts from 'typescript';
22
+ const NULLISH = ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Void;
23
+ /** The constituents of a union, or the type alone. */
24
+ export function constituentsOf(type) {
25
+ return type.isUnion() ? type.types : [type];
26
+ }
27
+ function missing(what) {
28
+ return { abstain: `result_member_missing: ${what}` };
29
+ }
30
+ /**
31
+ * Read `label` off a call's type. `raw` is the type as the call returns it and
32
+ * `awaited` the type after the transport unwrap; `at` is the node properties
33
+ * are instantiated at.
34
+ */
35
+ export function readResultLabel(checker, raw, awaited, label, at) {
36
+ if ('whole' in label)
37
+ return { type: awaited };
38
+ if ('member' in label)
39
+ return readMember(checker, awaited, label.member[0], at);
40
+ return readParse(checker, raw, awaited, label.parse, at);
41
+ }
42
+ function readMember(checker, awaited, name, at) {
43
+ const read = [];
44
+ for (const constituent of constituentsOf(awaited)) {
45
+ if (constituent.flags & NULLISH)
46
+ continue;
47
+ const property = checker.getPropertyOfType(constituent, name);
48
+ if (!property)
49
+ return missing(`the labelled member '${name}' is not on every constituent`);
50
+ const held = checker.getTypeOfSymbolAtLocation(property, at);
51
+ for (const part of constituentsOf(held)) {
52
+ if (!(part.flags & NULLISH))
53
+ read.push(part);
54
+ }
55
+ }
56
+ if (read.length === 0)
57
+ return missing(`the labelled member '${name}' holds no value`);
58
+ if (read.length === 1)
59
+ return { type: read[0] };
60
+ const union = checker.getUnionType;
61
+ // Without the checker's own union the members cannot be joined honestly;
62
+ // abstaining beats publishing the first of them.
63
+ if (!union)
64
+ return missing(`the labelled member '${name}' holds several types`);
65
+ return { type: union.call(checker, read) };
66
+ }
67
+ function readParse(checker, raw, awaited, name, at) {
68
+ // The method is looked up on the call's own type first: it is the thenable
69
+ // that carries `json()`, and its awaited value does not.
70
+ for (const holder of [raw, awaited]) {
71
+ const method = checker.getPropertyOfType(holder, name);
72
+ if (!method)
73
+ continue;
74
+ const signature = checker
75
+ .getSignaturesOfType(checker.getTypeOfSymbolAtLocation(method, at), ts.SignatureKind.Call)
76
+ .find((candidate) => candidate.parameters.every((p) => isOptional(p)));
77
+ if (!signature)
78
+ continue;
79
+ const returned = checker.getReturnTypeOfSignature(signature);
80
+ return { type: checker.getAwaitedType(returned) ?? returned };
81
+ }
82
+ return missing(`the labelled parse method '${name}' is not callable with no arguments`);
83
+ }
84
+ function isOptional(parameter) {
85
+ const declaration = parameter.valueDeclaration;
86
+ return (!!declaration &&
87
+ ts.isParameter(declaration) &&
88
+ (declaration.questionToken !== undefined ||
89
+ declaration.initializer !== undefined ||
90
+ declaration.dotDotDotToken !== undefined));
91
+ }
@@ -232,6 +232,17 @@ function runSelfCheck(args, treeFiles, repaired) {
232
232
  * because the module that is missing is no longer imported to be reported.
233
233
  * Fail closed: the alias keeps the refusal it had before the repair.
234
234
  */
235
+ /**
236
+ * Detail for a pinned external that does not resolve on an installed checkout.
237
+ * A subpath on the specifier means the rewrite found no entry of the package
238
+ * exporting a declaration from that file, so it named the file itself.
239
+ */
240
+ function unresolvedExternalDetail(spec) {
241
+ const pkg = packageNameOf(spec);
242
+ const subpath = spec.slice(pkg.length + 1);
243
+ const base = `unresolved pinned external '${spec}' on an installed checkout`;
244
+ return subpath ? `${base}: no entry of '${pkg}' exports a declaration from '${subpath}'` : base;
245
+ }
235
246
  function repairedNameFailure(repaired, file, diagnostic, message) {
236
247
  if (!repaired || diagnostic.code !== 2304)
237
248
  return undefined;
@@ -432,7 +443,7 @@ function checkedRecord(anchor, ctx) {
432
443
  }
433
444
  else {
434
445
  outcome = 'decayed_internal';
435
- detail = `unresolved pinned external '${blamedExternal}' on an installed checkout`;
446
+ detail = unresolvedExternalDetail(blamedExternal);
436
447
  }
437
448
  }
438
449
  else {
@@ -452,7 +463,7 @@ function checkedRecord(anchor, ctx) {
452
463
  }
453
464
  else {
454
465
  outcome = 'decayed_internal';
455
- detail = `unresolved pinned external '${blamedExternal}' on an installed checkout`;
466
+ detail = unresolvedExternalDetail(blamedExternal);
456
467
  }
457
468
  }
458
469
  return {