carrick 0.3.96 → 0.3.98

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 (49) hide show
  1. package/README.md +42 -282
  2. package/bin/carrick.mjs +19 -14
  3. package/dist/auth/run.d.ts +1 -1
  4. package/dist/auth/run.js +1 -1
  5. package/dist/contract.d.ts +15 -0
  6. package/dist/contract.js.map +1 -1
  7. package/dist/global-install.js +6 -5
  8. package/dist/global-install.js.map +1 -1
  9. package/dist/hook/reuse.d.ts +8 -3
  10. package/dist/hook/reuse.js +15 -3
  11. package/dist/hook/reuse.js.map +1 -1
  12. package/dist/init/connect.js +1 -1
  13. package/dist/init/connect.js.map +1 -1
  14. package/dist/init/doctor.js +5 -2
  15. package/dist/init/doctor.js.map +1 -1
  16. package/dist/init/hosted.d.ts +18 -1
  17. package/dist/init/hosted.js +84 -16
  18. package/dist/init/hosted.js.map +1 -1
  19. package/dist/init/install-id.d.ts +2 -2
  20. package/dist/init/install-id.js +3 -3
  21. package/dist/init/install-id.js.map +1 -1
  22. package/dist/init/mcp.d.ts +2 -2
  23. package/dist/init/mcp.js +2 -2
  24. package/dist/init/mcp.js.map +1 -1
  25. package/dist/init/outdated.d.ts +1 -1
  26. package/dist/init/outdated.js +1 -1
  27. package/dist/init/outdated.js.map +1 -1
  28. package/dist/init/output.d.ts +18 -1
  29. package/dist/init/output.js +40 -1
  30. package/dist/init/output.js.map +1 -1
  31. package/dist/init/remove.d.ts +71 -2
  32. package/dist/init/remove.js +474 -145
  33. package/dist/init/remove.js.map +1 -1
  34. package/dist/init/repo-copies.d.ts +16 -4
  35. package/dist/init/repo-copies.js +20 -6
  36. package/dist/init/repo-copies.js.map +1 -1
  37. package/dist/init/run.d.ts +12 -16
  38. package/dist/init/run.js +25 -42
  39. package/dist/init/run.js.map +1 -1
  40. package/dist/update.js +1 -0
  41. package/dist/update.js.map +1 -1
  42. package/package.json +7 -7
  43. package/plugin/.claude-plugin/plugin.json +1 -1
  44. package/sidecar/dist/src/client-semantics.d.ts +36 -0
  45. package/sidecar/dist/src/client-semantics.js +1008 -0
  46. package/sidecar/dist/src/index.js +35 -0
  47. package/sidecar/dist/src/types.d.ts +83 -2
  48. package/sidecar/dist/src/validators.d.ts +463 -0
  49. package/sidecar/dist/src/validators.js +50 -0
@@ -0,0 +1,1008 @@
1
+ /**
2
+ * `verify_client_semantics` (carrick#1564): check what a model said about an
3
+ * HTTP client library against the package's own type declarations.
4
+ *
5
+ * A claim names a member of a package export (or of an instance one of its
6
+ * factories returns) and says how that member is called: which option key
7
+ * carries the base URL, which HTTP method a verb sends, where the path, method
8
+ * and body sit. The scanner reads call sites through a claim only when this
9
+ * check verified it, and a verified claim becomes a fact that can fail a pull
10
+ * request check. So a claim is `verified` only when the declarations say so
11
+ * positively. `failed` means the declarations resolved and contradict the
12
+ * claim; `unchecked` means they could not be read, or say nothing at the place
13
+ * the claim needs (`any`, `unknown`, `{}`, an unconstrained type parameter).
14
+ * The scanner drops both.
15
+ *
16
+ * The export's type is read the way the service's own code would read it: a
17
+ * probe file in `from_dir` imports it, inside the service's program, under the
18
+ * service's compiler options. The declarations must be the installed
19
+ * package's: a `paths` alias or a `declare module` block the service writes
20
+ * for itself is not evidence about the library.
21
+ */
22
+ import * as fs from 'node:fs';
23
+ import * as path from 'node:path';
24
+ import { ts } from 'ts-morph';
25
+ /** The methods a `verb` claim may name, and a method key may accept. */
26
+ const HTTP_METHODS = new Set([
27
+ 'GET',
28
+ 'POST',
29
+ 'PUT',
30
+ 'PATCH',
31
+ 'DELETE',
32
+ 'HEAD',
33
+ 'OPTIONS',
34
+ ]);
35
+ /**
36
+ * Interfaces whose members every value of that kind inherits. A key found only
37
+ * on one of these (`constructor`, `toString`, a string's `length`) is not a
38
+ * key the library declares.
39
+ */
40
+ const BUILTIN_INTERFACES = new Set([
41
+ 'Object',
42
+ 'Function',
43
+ 'CallableFunction',
44
+ 'NewableFunction',
45
+ 'String',
46
+ 'Number',
47
+ 'Boolean',
48
+ 'Symbol',
49
+ 'BigInt',
50
+ 'Array',
51
+ 'ReadonlyArray',
52
+ ]);
53
+ /** A resolved module lands on TypeScript: a declaration file or source. */
54
+ const TYPESCRIPT_FILE = /\.(d\.[mc]?ts|[mc]?tsx?)$/;
55
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
56
+ const RECEIVER = /^(export|instance:\S+)$/;
57
+ const VERIFIED = { verdict: 'verified' };
58
+ const failed = (reason) => ({ verdict: 'failed', reason });
59
+ const unchecked = (reason) => ({ verdict: 'unchecked', reason });
60
+ /**
61
+ * A rest parameter typed by a type variable (`...rest: A`): the element at a
62
+ * position is `A[number]`, which says nothing the predicates can read.
63
+ */
64
+ const VARIADIC = Symbol('variadic element');
65
+ function furthest(current, rank, outcome) {
66
+ if (!current || rank > current.rank)
67
+ return { rank, outcome };
68
+ if (rank === current.rank && current.outcome.verdict === 'failed' && outcome.verdict === 'unchecked') {
69
+ return { rank, outcome };
70
+ }
71
+ return current;
72
+ }
73
+ let probeSequence = 0;
74
+ export class ClientSemanticsVerifier {
75
+ project;
76
+ constructor(project) {
77
+ this.project = project;
78
+ }
79
+ /**
80
+ * Judge every check, in request order, spending at most `budgetMs`. Checks
81
+ * the budget does not reach come back `unchecked` with reason `budget`.
82
+ */
83
+ run(fromDir, checks, budgetMs) {
84
+ const deadline = performance.now() + budgetMs;
85
+ const stamp = (check, outcome) => outcome.verdict === 'verified'
86
+ ? { claim_id: check.claim_id, receiver: check.receiver, verdict: 'verified' }
87
+ : {
88
+ claim_id: check.claim_id,
89
+ receiver: check.receiver,
90
+ verdict: outcome.verdict,
91
+ reason: outcome.reason,
92
+ };
93
+ if (checks.length === 0)
94
+ return { semantics: [], modules: [] };
95
+ // One import line per distinct (package, export), in first-seen order.
96
+ const importKeys = [];
97
+ const importIndex = new Map();
98
+ // The base-URL keys the request's factory claims name, per factory: an
99
+ // instance is read through the overload that declares them.
100
+ const factoryKeys = new Map();
101
+ for (const check of checks) {
102
+ const key = exportKey(check);
103
+ if (!importIndex.has(key)) {
104
+ importIndex.set(key, importKeys.length);
105
+ importKeys.push(key);
106
+ }
107
+ if (check.claim.kind === 'factory') {
108
+ const factory = factoryKey(key, check.claim.member);
109
+ const keys = factoryKeys.get(factory) ?? new Set();
110
+ keys.add(check.claim.base_url_key);
111
+ factoryKeys.set(factory, keys);
112
+ }
113
+ }
114
+ const probeText = importKeys
115
+ .map((key, i) => {
116
+ const [pkg, name] = JSON.parse(key);
117
+ return importLine(pkg, name, `__carrick_e${i}`);
118
+ })
119
+ .join('\n');
120
+ const probePath = path.join(fromDir, `__carrick_semantics_probe_${process.pid}_${probeSequence++}.ts`);
121
+ const probe = this.project.createSourceFile(probePath, `${probeText}\n`, { overwrite: true });
122
+ try {
123
+ const program = this.project.getProgram().compilerObject;
124
+ const file = program.getSourceFile(probe.getFilePath());
125
+ if (!file)
126
+ throw new Error(`probe file ${probePath} is not in the program`);
127
+ const reader = new DeclarationReader(program, file, this.project.getModuleResolutionHost(), fromDir);
128
+ const moduleReads = new Map();
129
+ const exportReads = new Map();
130
+ const receiverReads = new Map();
131
+ const semantics = [];
132
+ for (const check of checks) {
133
+ if (performance.now() >= deadline) {
134
+ semantics.push(stamp(check, unchecked('budget')));
135
+ continue;
136
+ }
137
+ if (!RECEIVER.test(check.receiver)) {
138
+ semantics.push(stamp(check, unchecked('receiver_invalid')));
139
+ continue;
140
+ }
141
+ const key = exportKey(check);
142
+ const declaration = file.statements[importIndex.get(key)];
143
+ reader.setPackage(check.package);
144
+ let moduleRead = moduleReads.get(check.package);
145
+ if (!moduleRead) {
146
+ moduleRead = reader.readModule(check.package, declaration);
147
+ moduleReads.set(check.package, moduleRead);
148
+ }
149
+ if (moduleRead.reason) {
150
+ semantics.push(stamp(check, unchecked(moduleRead.reason)));
151
+ continue;
152
+ }
153
+ let exportRead = exportReads.get(key);
154
+ if (!exportRead) {
155
+ exportRead = reader.readExport(declaration);
156
+ exportReads.set(key, exportRead);
157
+ }
158
+ if ('reason' in exportRead) {
159
+ semantics.push(stamp(check, unchecked(exportRead.reason)));
160
+ continue;
161
+ }
162
+ const receiverKey = `${key}\u0000${check.receiver}`;
163
+ let receiverRead = receiverReads.get(receiverKey);
164
+ if (!receiverRead) {
165
+ const factory = check.receiver.startsWith('instance:')
166
+ ? check.receiver.slice('instance:'.length)
167
+ : undefined;
168
+ receiverRead = reader.readReceiver(exportRead.value, factory, factory === undefined ? undefined : factoryKeys.get(factoryKey(key, factory)));
169
+ receiverReads.set(receiverKey, receiverRead);
170
+ }
171
+ if ('reason' in receiverRead) {
172
+ semantics.push(stamp(check, unchecked(receiverRead.reason)));
173
+ continue;
174
+ }
175
+ semantics.push(stamp(check, reader.judge(receiverRead.value, check.claim)));
176
+ }
177
+ return {
178
+ semantics,
179
+ modules: [...moduleReads.values()].map(read => read.entry),
180
+ };
181
+ }
182
+ finally {
183
+ this.project.removeSourceFile(probe);
184
+ }
185
+ }
186
+ }
187
+ function exportKey(check) {
188
+ return JSON.stringify([check.package, check.export]);
189
+ }
190
+ function factoryKey(exportKeyText, member) {
191
+ return `${exportKeyText}\u0000${member}`;
192
+ }
193
+ /** The probe's import of one export, bound to `local`. */
194
+ function importLine(pkg, name, local) {
195
+ const specifier = JSON.stringify(pkg);
196
+ if (name === 'default')
197
+ return `import ${local} from ${specifier};`;
198
+ const imported = IDENTIFIER.test(name) ? name : JSON.stringify(name);
199
+ return `import { ${imported} as ${local} } from ${specifier};`;
200
+ }
201
+ /** Upper-case ASCII letters only: `ſ` and `ı` must not become `S` and `I`. */
202
+ function asciiUpperCase(text) {
203
+ return text.replace(/[a-z]/g, letter => letter.toUpperCase());
204
+ }
205
+ /**
206
+ * Reads the declarations the probe imported, with the program's own checker.
207
+ * Every predicate is the contract's (carrick#1564, section 3), stated on the
208
+ * checker's public API, with the definitions the review amended.
209
+ */
210
+ class DeclarationReader {
211
+ program;
212
+ probe;
213
+ host;
214
+ checker;
215
+ /** The service root, as a realpath. */
216
+ root;
217
+ packageDirectories = new Map();
218
+ realpaths = new Map();
219
+ /** Names the service declares in its own blocks, per augmented package (`global` for `declare global`). */
220
+ augmentedNames;
221
+ /** The package of the check being judged; see `serviceAugmentedNames`. */
222
+ currentPackage = '';
223
+ constructor(program, probe, host, serviceRoot) {
224
+ this.program = program;
225
+ this.probe = probe;
226
+ this.host = host;
227
+ this.checker = program.getTypeChecker();
228
+ this.root = this.realpath(serviceRoot);
229
+ }
230
+ /**
231
+ * A path with its symlinks resolved. The compiler spells the service's own
232
+ * files as given and resolved dependencies as realpaths, so both sides of a
233
+ * comparison go through here. A path not on disk (the default library ts-morph
234
+ * serves from memory) is kept as it is.
235
+ */
236
+ realpath(fileName) {
237
+ let real = this.realpaths.get(fileName);
238
+ if (real === undefined) {
239
+ try {
240
+ real = fs.realpathSync(fileName);
241
+ }
242
+ catch {
243
+ real = fileName;
244
+ }
245
+ this.realpaths.set(fileName, real);
246
+ }
247
+ return real;
248
+ }
249
+ // --------------------------------------------------------------------------
250
+ // Resolve, export, receiver
251
+ // --------------------------------------------------------------------------
252
+ /**
253
+ * Where the package resolved. The checker's module symbol is what the
254
+ * service's program imports; its primary declaration must be the file the
255
+ * module resolver lands on, and the resolver must have reached it as an
256
+ * installed dependency. A `paths` alias to the service's own code, or a
257
+ * `declare module` block the service writes, stands in for the package and
258
+ * says nothing about it (`module_local`). The resolver's flag is read, not
259
+ * `program.isSourceFileFromExternalLibrary`: once ts-morph has loaded a
260
+ * dependency, the next program lists it as a root file and the program no
261
+ * longer calls it external. `resolved_file` and `installed_version` both
262
+ * come from that one agreeing answer.
263
+ */
264
+ readModule(pkg, declaration) {
265
+ const specifier = declaration.moduleSpecifier;
266
+ const resolved = ts.resolveModuleName(pkg, this.probe.fileName, this.program.getCompilerOptions(), this.host, undefined, undefined, this.program.getModeForUsageLocation(this.probe, specifier)).resolvedModule;
267
+ const entry = { package: pkg };
268
+ const done = (reason) => reason ? { entry: { ...entry, reason }, reason } : { entry };
269
+ const moduleSymbol = this.checker.getSymbolAtLocation(specifier);
270
+ const declarations = moduleSymbol?.declarations ?? [];
271
+ // The module itself, not a service-side augmentation of it.
272
+ const primary = declarations.find(ts.isSourceFile) ?? declarations[0];
273
+ const file = primary?.getSourceFile();
274
+ if (file) {
275
+ entry.resolved_file = file.fileName;
276
+ const agrees = resolved !== undefined && this.isSameInstalledFile(resolved.resolvedFileName, file);
277
+ // Under `node_modules` is not enough: a `paths` alias can point the name
278
+ // at another installed package. The resolver must land in an installed
279
+ // package directory, and that package must be the one named: by its
280
+ // `packageId` (or its separate `@types` package), or, for an `npm:`
281
+ // alias, by the directory name, which the installer takes from the
282
+ // alias. (The package.json comparison beside it always holds:
283
+ // TypeScript builds `packageId` from that same package.json.)
284
+ const installed = agrees && resolved !== undefined ? this.installedPackage(resolved.resolvedFileName) : undefined;
285
+ const packageId = resolved?.packageId;
286
+ const named = installed !== undefined &&
287
+ packageId !== undefined &&
288
+ (isNamedPackage(pkg, packageId.name) ||
289
+ (installed.directoryName === packageNameOf(pkg) && installed.packageName === packageId.name));
290
+ if (named && packageId.version)
291
+ entry.installed_version = packageId.version;
292
+ if (!TYPESCRIPT_FILE.test(file.fileName))
293
+ return done('module_js_only');
294
+ if (!named)
295
+ return done('module_local');
296
+ return done();
297
+ }
298
+ if (!resolved)
299
+ return done('module_unresolved');
300
+ entry.resolved_file = resolved.resolvedFileName;
301
+ const version = resolved.packageId?.version;
302
+ if (version)
303
+ entry.installed_version = version;
304
+ if (!TYPESCRIPT_FILE.test(resolved.resolvedFileName))
305
+ return done('module_js_only');
306
+ // A declaration file with no module symbol exports nothing: every export
307
+ // of it is missing.
308
+ return done();
309
+ }
310
+ /**
311
+ * The resolver's file is the checker's file. With two installs of the same
312
+ * name and version, TypeScript loads one and makes the other a redirect to
313
+ * it, so the resolver's copy may be a redirect whose target is the checker's
314
+ * file; that counts only when the target is itself an installed file.
315
+ */
316
+ isSameInstalledFile(resolvedFileName, file) {
317
+ const resolved = this.program.getSourceFile(resolvedFileName);
318
+ if (resolved === file)
319
+ return true;
320
+ // `redirectInfo` is internal to the compiler, but it is the only record of
321
+ // which copy TypeScript deduplicated a package into.
322
+ const target = resolved
323
+ ?.redirectInfo?.redirectTarget;
324
+ return target === file && this.isInstalledFile(file.fileName);
325
+ }
326
+ /** `T`: the type the probe's import gets. */
327
+ readExport(declaration) {
328
+ const clause = declaration.importClause;
329
+ const bindings = clause?.namedBindings;
330
+ const local = clause?.name ??
331
+ (bindings && ts.isNamedImports(bindings) ? bindings.elements[0]?.name : undefined);
332
+ const alias = local ? this.checker.getSymbolAtLocation(local) : undefined;
333
+ if (!local || !alias)
334
+ return { reason: 'export_missing' };
335
+ const target = this.checker.getAliasedSymbol(alias);
336
+ // A name the module does not export resolves to the checker's unknown
337
+ // symbol; a type-only export has no value to call.
338
+ if (this.checker.isUnknownSymbol(target) || !(target.flags & ts.SymbolFlags.Value)) {
339
+ return { reason: 'export_missing' };
340
+ }
341
+ const type = this.checker.getTypeOfSymbolAtLocation(alias, local);
342
+ if (this.isOpenTop(type))
343
+ return { reason: 'export_untyped' };
344
+ return { value: type };
345
+ }
346
+ /**
347
+ * `R`: the export itself, or what its factory `factory` returns. The
348
+ * instance comes from the first overload whose first parameter is an object
349
+ * type (or the only overload). When a `factory` claim in the request for the
350
+ * same factory holds, the overload that satisfies it must be that same one,
351
+ * or the instance is unresolved: a base URL set through one overload says
352
+ * nothing about the instance another overload builds. A factory claim that
353
+ * holds on no overload leaves the rule as it is; the scanner never reads an
354
+ * instance through it.
355
+ */
356
+ readReceiver(exported, factory, baseUrlKeys) {
357
+ if (factory === undefined)
358
+ return { value: exported };
359
+ const callable = this.callableProperty(exported, factory);
360
+ if ('failure' in callable)
361
+ return { reason: 'factory_unresolved' };
362
+ const signatures = callable.signatures;
363
+ const signature = signatures.find(sig => {
364
+ const first = this.parameterAt(sig, 0);
365
+ return first !== undefined && this.isObjectType(first);
366
+ }) ?? (signatures.length === 1 ? signatures[0] : undefined);
367
+ if (!signature)
368
+ return { reason: 'factory_unresolved' };
369
+ for (const baseUrlKey of baseUrlKeys ?? []) {
370
+ const keyed = signatures.find(sig => this.factorySignatureOutcome(sig, baseUrlKey).rank === Infinity);
371
+ if (keyed !== undefined && keyed !== signature)
372
+ return { reason: 'factory_unresolved' };
373
+ }
374
+ const instance = this.checker.getReturnTypeOfSignature(signature);
375
+ if (this.returnSaysNothing(instance))
376
+ return { reason: 'factory_unresolved' };
377
+ return { value: instance };
378
+ }
379
+ // --------------------------------------------------------------------------
380
+ // Claims
381
+ // --------------------------------------------------------------------------
382
+ judge(receiver, claim) {
383
+ switch (claim.kind) {
384
+ case 'factory':
385
+ return this.judgeFactory(receiver, claim.member, claim.base_url_key);
386
+ case 'verb':
387
+ return this.judgeVerb(receiver, claim.member, claim.method);
388
+ case 'verb_body':
389
+ return this.judgeVerbBody(receiver, claim.member, claim.args, claim.body_key);
390
+ case 'request': {
391
+ const selected = this.selectRequest(receiver, claim.member, claim.args, claim.url_key, claim.method_key);
392
+ return 'failure' in selected ? selected.failure : VERIFIED;
393
+ }
394
+ case 'request_body': {
395
+ const selected = this.selectRequest(receiver, claim.member, claim.args, claim.url_key, claim.method_key, claim.body_key);
396
+ return 'failure' in selected ? selected.failure : VERIFIED;
397
+ }
398
+ }
399
+ }
400
+ /**
401
+ * `member` is a declared callable property of the receiver; some signature's
402
+ * first parameter declares `base_url_key` accepting string, and returns a
403
+ * type that says something.
404
+ */
405
+ judgeFactory(receiver, member, baseUrlKey) {
406
+ const callable = this.callableProperty(receiver, member);
407
+ if ('failure' in callable)
408
+ return callable.failure;
409
+ let best;
410
+ for (const signature of callable.signatures) {
411
+ const result = this.factorySignatureOutcome(signature, baseUrlKey);
412
+ if (result.rank === Infinity)
413
+ return VERIFIED;
414
+ best = furthest(best, result.rank, result.outcome);
415
+ }
416
+ return best.outcome;
417
+ }
418
+ /** One factory overload against the factory predicate; rank `Infinity` holds. */
419
+ factorySignatureOutcome(signature, baseUrlKey) {
420
+ const first = this.keyParameterAt(signature, 0);
421
+ if (first === undefined)
422
+ return { rank: 1, outcome: failed('param_missing') };
423
+ const key = this.keyProperty(first, baseUrlKey, type => this.acceptsString(type));
424
+ if (!key)
425
+ return { rank: 2, outcome: this.slotFailure(first, 'key_missing') };
426
+ const keyType = this.checker.getTypeOfSymbol(key);
427
+ if (!this.acceptsString(keyType))
428
+ return { rank: 3, outcome: this.slotFailure(keyType, 'key_not_string') };
429
+ // The option is there, but what the factory builds cannot be read, so
430
+ // nothing it returns can be checked either.
431
+ if (this.returnSaysNothing(this.checker.getReturnTypeOfSignature(signature))) {
432
+ return { rank: 4, outcome: unchecked('factory_unresolved') };
433
+ }
434
+ return { rank: Infinity, outcome: VERIFIED };
435
+ }
436
+ /**
437
+ * `method` is `member` upper-cased (ASCII only) and an HTTP method (a method
438
+ * is not a type-level fact, so this is read off the claim itself); `member`
439
+ * is a declared callable property whose first parameter accepts string.
440
+ */
441
+ judgeVerb(receiver, member, method) {
442
+ if (method !== asciiUpperCase(member) || !HTTP_METHODS.has(method)) {
443
+ return failed('method_not_member_verb');
444
+ }
445
+ const callable = this.callableProperty(receiver, member);
446
+ if ('failure' in callable)
447
+ return callable.failure;
448
+ let best;
449
+ for (const signature of callable.signatures) {
450
+ const first = this.keyParameterAt(signature, 0);
451
+ if (first !== undefined && this.acceptsString(first))
452
+ return VERIFIED;
453
+ best = furthest(best, 1, first === undefined ? failed('path_not_string') : this.slotFailure(first, 'path_not_string'));
454
+ }
455
+ return best.outcome;
456
+ }
457
+ /**
458
+ * Some signature whose first parameter accepts string has a second one:
459
+ * open for `path_body`; for `path_options`, an object type with at least one
460
+ * declared property, `body_key` among them when given.
461
+ */
462
+ judgeVerbBody(receiver, member, args, bodyKey) {
463
+ const callable = this.callableProperty(receiver, member);
464
+ if ('failure' in callable)
465
+ return callable.failure;
466
+ let best;
467
+ for (const signature of callable.signatures) {
468
+ const first = this.keyParameterAt(signature, 0);
469
+ // An open body is read as declared: a type parameter is the open body
470
+ // itself, not its constraint.
471
+ const second = args === 'path_body' ? this.parameterAt(signature, 1) : this.keyParameterAt(signature, 1);
472
+ if (first === undefined || second === undefined) {
473
+ best = furthest(best, 1, failed('param_missing'));
474
+ continue;
475
+ }
476
+ if (!this.acceptsString(first)) {
477
+ best = furthest(best, 1, this.slotFailure(first, 'param_missing'));
478
+ continue;
479
+ }
480
+ if (args === 'path_body') {
481
+ if (this.isOpenBody(second))
482
+ return VERIFIED;
483
+ best = furthest(best, 2, this.slotFailure(second, 'body_not_open'));
484
+ continue;
485
+ }
486
+ if (!this.isObjectType(second) || this.declaredProperties(second).length === 0) {
487
+ best = furthest(best, 2, this.slotFailure(second, 'options_not_object'));
488
+ continue;
489
+ }
490
+ if (bodyKey !== undefined && !this.declaredProperty(second, bodyKey)) {
491
+ best = furthest(best, 3, failed('key_missing'));
492
+ continue;
493
+ }
494
+ return VERIFIED;
495
+ }
496
+ return best.outcome;
497
+ }
498
+ /**
499
+ * The callee's signatures a request claim can be read through. The callee
500
+ * is `receiver[member]`, or the receiver itself when `member` is null.
501
+ *
502
+ * `config`: the first parameter is an object type declaring `url_key`
503
+ * accepting string and `method_key` accepting string or an HTTP method
504
+ * literal. `path_options`: the first parameter accepts string and the second
505
+ * is an object type declaring `method_key` accepting the same. A
506
+ * `request_body` claim also needs `body_key` declared there.
507
+ *
508
+ * All the keys come from ONE config object: for a union, from one member
509
+ * that is an object type and not a function type. Keys split across union
510
+ * members (`{ url } | { method }`) describe no call anyone can make.
511
+ */
512
+ selectRequest(receiver, member, args, urlKey, methodKey, bodyKey) {
513
+ const callable = member === null ? this.callSignatures(receiver) : this.callableProperty(receiver, member);
514
+ if ('failure' in callable)
515
+ return callable;
516
+ const selected = [];
517
+ let best;
518
+ for (const signature of callable.signatures) {
519
+ const first = this.keyParameterAt(signature, 0);
520
+ let config = first;
521
+ if (args === 'path_options') {
522
+ if (first === undefined || !this.acceptsString(first)) {
523
+ best = furthest(best, 1, first === undefined ? failed('param_missing') : this.slotFailure(first, 'param_missing'));
524
+ continue;
525
+ }
526
+ config = this.keyParameterAt(signature, 1);
527
+ }
528
+ if (config === undefined) {
529
+ best = furthest(best, 1, failed('param_missing'));
530
+ continue;
531
+ }
532
+ if (config === VARIADIC || !this.isObjectType(config)) {
533
+ best = furthest(best, 1, this.slotFailure(config, 'param_missing'));
534
+ continue;
535
+ }
536
+ const parts = this.parts(config);
537
+ const views = parts.length > 1 ? parts.filter(part => this.isPlainObject(part)) : [config];
538
+ if (views.length === 0)
539
+ best = furthest(best, 2, failed('key_missing'));
540
+ for (const view of views) {
541
+ const outcome = this.requestKeysOutcome(view, args, urlKey, methodKey, bodyKey);
542
+ if (outcome.rank === Infinity) {
543
+ selected.push(signature);
544
+ break;
545
+ }
546
+ best = furthest(best, outcome.rank, outcome.outcome);
547
+ }
548
+ }
549
+ if (selected.length > 0)
550
+ return { signatures: selected };
551
+ return { failure: best.outcome };
552
+ }
553
+ /** One config object against a request claim's keys; rank `Infinity` holds. */
554
+ requestKeysOutcome(config, args, urlKey, methodKey, bodyKey) {
555
+ // A `config` claim names its url key; with none there is nothing to find.
556
+ const url = args === 'config'
557
+ ? urlKey === undefined
558
+ ? undefined
559
+ : this.declaredProperty(config, urlKey)
560
+ : null;
561
+ const method = this.declaredProperty(config, methodKey);
562
+ if (url === undefined || !method)
563
+ return { rank: 2, outcome: failed('key_missing') };
564
+ const urlType = url === null ? undefined : this.checker.getTypeOfSymbol(url);
565
+ if (urlType !== undefined && !this.acceptsString(urlType)) {
566
+ return { rank: 3, outcome: this.slotFailure(urlType, 'key_not_string') };
567
+ }
568
+ const methodType = this.checker.getTypeOfSymbol(method);
569
+ if (!this.acceptsMethod(methodType)) {
570
+ return { rank: 3, outcome: this.slotFailure(methodType, 'key_not_string') };
571
+ }
572
+ if (bodyKey !== undefined && !this.declaredProperty(config, bodyKey)) {
573
+ return { rank: 4, outcome: failed('key_missing') };
574
+ }
575
+ return { rank: Infinity, outcome: VERIFIED };
576
+ }
577
+ // --------------------------------------------------------------------------
578
+ // Definitions
579
+ // --------------------------------------------------------------------------
580
+ /**
581
+ * A declared property `name` of `type` whose type has a call signature. A
582
+ * member typed `any`, `unknown` or `{}` says nothing (`member_untyped`).
583
+ */
584
+ callableProperty(type, name) {
585
+ const property = this.declaredProperty(type, name);
586
+ if (!property)
587
+ return { failure: failed('member_missing') };
588
+ return this.callSignatures(this.checker.getTypeOfSymbol(property));
589
+ }
590
+ /**
591
+ * The call signatures the library declares. A service `declare module`
592
+ * block can add an overload to a library member; that signature is the
593
+ * service's claim about the library, not the library's.
594
+ */
595
+ callSignatures(type) {
596
+ const signatures = this.checker
597
+ .getNonNullableType(type)
598
+ .getCallSignatures()
599
+ .filter(signature => {
600
+ const declaration = signature.getDeclaration();
601
+ return declaration !== undefined && this.isLibraryFile(declaration.getSourceFile());
602
+ });
603
+ if (signatures.length > 0)
604
+ return { signatures };
605
+ return { failure: this.slotFailure(type, 'member_not_callable') };
606
+ }
607
+ /**
608
+ * A declared property of `type`: one the checker lists on its apparent type
609
+ * with null and undefined removed, and not one every value of that kind
610
+ * inherits (`constructor`, `toString`, a primitive wrapper's members). An
611
+ * index signature does not count. Nor does a property typed `never` (or
612
+ * only `undefined`): it cannot be passed. And a library declares it: at
613
+ * least one declaration sits in an installed package or the default
614
+ * library, so a member only the service's own module augmentation adds is
615
+ * not the package's.
616
+ */
617
+ declaredProperty(slot, name) {
618
+ return this.declaredProperties(slot).find(property => property.getName() === name);
619
+ }
620
+ /**
621
+ * The property `name` of a parameter a key is looked for on. It is the
622
+ * declared property of the whole type; failing that, for a union, one that an
623
+ * object constituent declares and whose type passes `accepts`. A constituent
624
+ * counts only when it is an object type that says something and is not a
625
+ * function type, so defaults written as `Options | ((parent) => Options)`
626
+ * are read through `Options`. When a constituent declares the name but no
627
+ * declaration passes `accepts`, that property is returned for the caller to
628
+ * reject.
629
+ */
630
+ keyProperty(slot, name, accepts) {
631
+ const whole = this.declaredProperty(slot, name);
632
+ if (whole || slot === VARIADIC)
633
+ return whole;
634
+ let declaredOnly;
635
+ for (const part of this.parts(slot)) {
636
+ if (!this.isPlainObject(part))
637
+ continue;
638
+ const property = this.declaredProperty(part, name);
639
+ if (!property)
640
+ continue;
641
+ if (accepts(this.checker.getTypeOfSymbol(property)))
642
+ return property;
643
+ declaredOnly ??= property;
644
+ }
645
+ return declaredOnly;
646
+ }
647
+ /**
648
+ * An object type that is not a function type. (One that says nothing
649
+ * declares no property, so it never supplies a key.)
650
+ */
651
+ isPlainObject(type) {
652
+ return (this.isObjectLike(type) &&
653
+ type.getCallSignatures().length === 0 &&
654
+ type.getConstructSignatures().length === 0);
655
+ }
656
+ /**
657
+ * A parameter a key is looked for on, or that must accept a string: read
658
+ * through a rest parameter's element and through a type parameter's
659
+ * constraint. A type parameter with no constraint, or one of `any` or
660
+ * `unknown`, stays as it is and says nothing.
661
+ */
662
+ keyParameterAt(signature, index) {
663
+ const slot = this.parameterAt(signature, index, true);
664
+ return slot === undefined ? undefined : this.throughConstraint(slot);
665
+ }
666
+ throughConstraint(slot) {
667
+ if (slot === VARIADIC)
668
+ return slot;
669
+ const parts = this.parts(slot);
670
+ if (parts.length !== 1 || !(parts[0].flags & ts.TypeFlags.TypeParameter))
671
+ return slot;
672
+ // An `any` or `unknown` constraint says nothing, as the bare parameter does.
673
+ return this.checker.getBaseConstraintOfType(parts[0]) ?? slot;
674
+ }
675
+ declaredProperties(slot) {
676
+ if (slot === VARIADIC)
677
+ return [];
678
+ const apparent = this.checker.getApparentType(this.checker.getNonNullableType(slot));
679
+ return this.checker
680
+ .getPropertiesOfType(apparent)
681
+ .filter(property => !this.isBuiltinMember(property) &&
682
+ this.isLibraryDeclared(property, apparent) &&
683
+ !this.isAbsent(property));
684
+ }
685
+ /**
686
+ * Some declaration of the property is in an installed package or the
687
+ * default library. A member a mapped type makes (`extends Record<'get',
688
+ * Fn>`) has no declaration of its own; it counts when the type listing it
689
+ * was written by the library.
690
+ */
691
+ isLibraryDeclared(property, listing) {
692
+ const declarations = property.declarations ?? [];
693
+ if (declarations.length === 0) {
694
+ return (!this.isNamedByServiceAugmentation(property.getName()) &&
695
+ this.isListedByLibraryType(listing, property.getName()));
696
+ }
697
+ return declarations.some(declaration => this.isLibraryFile(declaration.getSourceFile()));
698
+ }
699
+ /** Judge the next check as a claim about `pkg`. */
700
+ setPackage(pkg) {
701
+ this.currentPackage = packageNameOf(pkg);
702
+ }
703
+ /**
704
+ * The service's own `declare module '<this package>'` (or one of its
705
+ * subpaths) or `declare global` blocks declare a member of this name,
706
+ * compared without case. A mapped type's member has no declaration of its
707
+ * own, so when the service adds a key to the interface a library mapped
708
+ * type iterates (`Record<keyof MethodMap, Fn>`, with or without `& string`,
709
+ * or re-cased by an `as Lowercase<...>` remap), nothing on the member says
710
+ * the service put it there. Blocks for other packages do not count: a
711
+ * service augments many packages, and their member names say nothing about
712
+ * this one.
713
+ */
714
+ isNamedByServiceAugmentation(name) {
715
+ const names = this.serviceAugmentedNames();
716
+ const lower = name.toLowerCase();
717
+ return Boolean(names.get(this.currentPackage)?.has(lower) || names.get('global')?.has(lower));
718
+ }
719
+ serviceAugmentedNames() {
720
+ if (this.augmentedNames)
721
+ return this.augmentedNames;
722
+ const byPackage = new Map();
723
+ const collect = (node, names) => {
724
+ if ((ts.isPropertySignature(node) ||
725
+ ts.isMethodSignature(node) ||
726
+ ts.isPropertyDeclaration(node) ||
727
+ ts.isMethodDeclaration(node) ||
728
+ ts.isEnumMember(node)) &&
729
+ (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) || ts.isNumericLiteral(node.name))) {
730
+ names.add(node.name.text.toLowerCase());
731
+ }
732
+ ts.forEachChild(node, child => collect(child, names));
733
+ };
734
+ for (const file of this.program.getSourceFiles()) {
735
+ if (file === this.probe || this.isLibraryFile(file))
736
+ continue;
737
+ for (const statement of file.statements) {
738
+ if (!ts.isModuleDeclaration(statement) || !statement.body)
739
+ continue;
740
+ const key = ts.isStringLiteral(statement.name)
741
+ ? packageNameOf(statement.name.text)
742
+ : statement.flags & ts.NodeFlags.GlobalAugmentation
743
+ ? 'global'
744
+ : undefined;
745
+ if (key === undefined)
746
+ continue;
747
+ let names = byPackage.get(key);
748
+ if (!names)
749
+ byPackage.set(key, (names = new Set()));
750
+ collect(statement.body, names);
751
+ }
752
+ }
753
+ this.augmentedNames = byPackage;
754
+ return byPackage;
755
+ }
756
+ /**
757
+ * Member `name`, which has no declaration of its own, reaches `listing`
758
+ * along a path of types the library alone declares. The path runs through
759
+ * the types that list the member: an intersection's parts
760
+ * (`type Client = {...} & Record<Alias, Fn> & Fn`), and an interface's base
761
+ * types (`interface S extends Base`), each declared only in library files.
762
+ * The library's `Record` is; a base interface the service extended with its
763
+ * own mapped type is not, at any depth.
764
+ */
765
+ isListedByLibraryType(listing, name) {
766
+ const listedBy = (types) => types.some(part => {
767
+ const apparent = this.checker.getApparentType(part);
768
+ return (this.checker.getPropertyOfType(apparent, name) !== undefined &&
769
+ this.isListedByLibraryType(apparent, name));
770
+ });
771
+ if (listing.isIntersection())
772
+ return listedBy(listing.types);
773
+ if (!this.isDeclaredOnlyInLibrary(listing.getSymbol()))
774
+ return false;
775
+ const objectFlags = listing.objectFlags ?? 0;
776
+ const target = objectFlags & ts.ObjectFlags.Reference ? listing.target : listing;
777
+ const targetFlags = target.objectFlags ?? 0;
778
+ // A type literal or mapped type lists its members itself.
779
+ if (!(targetFlags & ts.ObjectFlags.ClassOrInterface))
780
+ return true;
781
+ return listedBy(this.checker.getBaseTypes(target));
782
+ }
783
+ isDeclaredOnlyInLibrary(symbol) {
784
+ const declarations = symbol?.declarations ?? [];
785
+ return (declarations.length > 0 &&
786
+ declarations.every(declaration => this.isLibraryFile(declaration.getSourceFile())));
787
+ }
788
+ /** An installed package's file, or the default library's. */
789
+ isLibraryFile(file) {
790
+ return this.program.isSourceFileDefaultLibrary(file) || this.isInstalledFile(file.fileName);
791
+ }
792
+ isInstalledFile(fileName) {
793
+ return this.installedPackage(fileName) !== undefined;
794
+ }
795
+ /**
796
+ * The installed package a file belongs to. Its path, taken relative to the
797
+ * service root, has a package directory after its last `node_modules`
798
+ * segment (two segments for a scope), holding a package.json. A service
799
+ * source file in a directory that happens to be named `node_modules`, or a
800
+ * repository checked out under a `node_modules` ancestor, is not installed.
801
+ * An install hoisted above the service root (`../../node_modules/pkg`) and
802
+ * a pnpm store (`node_modules/.pnpm/pkg@1/node_modules/pkg`) are.
803
+ */
804
+ installedPackage(fileName) {
805
+ const segments = path.relative(this.root, this.realpath(fileName)).split(path.sep);
806
+ const last = segments.lastIndexOf('node_modules');
807
+ if (last < 0)
808
+ return undefined;
809
+ const width = segments[last + 1]?.startsWith('@') ? 2 : 1;
810
+ const nameSegments = segments.slice(last + 1, last + 1 + width);
811
+ // No package.json there (a file directly under node_modules included) means no package.
812
+ const directory = path.resolve(this.root, ...segments.slice(0, last + 1 + width));
813
+ let known = this.packageDirectories.get(directory);
814
+ if (known === undefined) {
815
+ known = readInstalledPackage(this.host, directory, nameSegments.join('/'));
816
+ this.packageDirectories.set(directory, known);
817
+ }
818
+ return known ?? undefined;
819
+ }
820
+ /** Typed so that nothing can be passed: `never`, or only `undefined`. */
821
+ isAbsent(property) {
822
+ return this.parts(this.checker.getTypeOfSymbol(property)).every(part => (part.flags & ts.TypeFlags.Never) !== 0);
823
+ }
824
+ isBuiltinMember(property) {
825
+ const declarations = property.declarations ?? [];
826
+ return (declarations.length > 0 &&
827
+ declarations.every(declaration => {
828
+ const owner = declaration.parent;
829
+ if (!owner || !ts.isInterfaceDeclaration(owner))
830
+ return false;
831
+ const symbol = this.checker.getSymbolAtLocation(owner.name);
832
+ return symbol !== undefined && BUILTIN_INTERFACES.has(this.checker.getFullyQualifiedName(symbol));
833
+ }));
834
+ }
835
+ /**
836
+ * `string` is assignable to the type, and some part of it besides null and
837
+ * undefined is string-like. `any`, `unknown`, `{}` and `Object` accept a
838
+ * string without saying anything about one.
839
+ */
840
+ acceptsString(declared) {
841
+ const slot = this.throughConstraint(declared);
842
+ if (slot === VARIADIC)
843
+ return false;
844
+ return (this.checker.isTypeAssignableTo(this.checker.getStringType(), slot) &&
845
+ this.parts(slot).some(part => isStringLike(part)));
846
+ }
847
+ /** Accepts string, or names an HTTP method as a literal in either case. */
848
+ acceptsMethod(declared) {
849
+ const slot = this.throughConstraint(declared);
850
+ if (slot === VARIADIC)
851
+ return false;
852
+ if (this.acceptsString(slot))
853
+ return true;
854
+ return this.parts(slot).some(part => part.isStringLiteral() && HTTP_METHODS.has(asciiUpperCase(part.value)));
855
+ }
856
+ /**
857
+ * A body parameter that takes any payload: `unknown`, or a type parameter
858
+ * with no constraint (or one of `unknown` or `any`). `any` itself is not
859
+ * open: a declared `any`, an unresolved type and a defaulted type argument
860
+ * all read as `any`, and none of them says the parameter is a body.
861
+ */
862
+ isOpenBody(slot) {
863
+ if (slot === VARIADIC)
864
+ return false;
865
+ const parts = this.parts(slot);
866
+ return (parts.length > 0 &&
867
+ parts.every(part => (part.flags & ts.TypeFlags.Unknown) !== 0 || this.isUnconstrained(part)));
868
+ }
869
+ /** Every part besides null and undefined is an object type. */
870
+ isObjectType(slot) {
871
+ if (slot === VARIADIC)
872
+ return false;
873
+ const parts = this.parts(slot);
874
+ return parts.length > 0 && parts.every(part => this.isObjectLike(part));
875
+ }
876
+ isObjectLike(type) {
877
+ if (type.flags & ts.TypeFlags.Object)
878
+ return true;
879
+ if (type.isIntersection())
880
+ return type.types.every(part => this.isObjectLike(part));
881
+ if (type.flags & ts.TypeFlags.TypeParameter) {
882
+ const constraint = this.checker.getBaseConstraintOfType(type);
883
+ return constraint !== undefined && constraint !== type && this.isObjectLike(constraint);
884
+ }
885
+ return false;
886
+ }
887
+ /**
888
+ * The verdict when a slot fails its predicate: `unchecked` (`member_untyped`)
889
+ * when its type says nothing, `failed` with `code` when it says something
890
+ * else.
891
+ */
892
+ slotFailure(slot, code) {
893
+ return this.saysNothing(slot) ? unchecked('member_untyped') : failed(code);
894
+ }
895
+ /**
896
+ * Some part of the type, besides null and undefined, is `any`, `unknown`,
897
+ * an unconstrained type parameter, or an object type with nothing declared
898
+ * on it (`{}`, `Object`, `Function`).
899
+ */
900
+ saysNothing(slot) {
901
+ if (slot === VARIADIC)
902
+ return true;
903
+ return this.parts(slot).some(part => this.isOpenTop(part) || this.isUnconstrained(part) || this.isEmptyObject(part));
904
+ }
905
+ /**
906
+ * What a factory builds says nothing: the type itself, or any branch of a
907
+ * conditional return type, does.
908
+ */
909
+ returnSaysNothing(type) {
910
+ if (this.saysNothing(type))
911
+ return true;
912
+ return this.parts(type).some(part => {
913
+ if (!(part.flags & ts.TypeFlags.Conditional))
914
+ return false;
915
+ const node = part.root.node;
916
+ return [node.trueType, node.falseType].some(branch => this.returnSaysNothing(this.checker.getTypeFromTypeNode(branch)));
917
+ });
918
+ }
919
+ /** `any` (an unresolved type included) or `unknown`. */
920
+ isOpenTop(type) {
921
+ return (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
922
+ }
923
+ isUnconstrained(type) {
924
+ if (!(type.flags & ts.TypeFlags.TypeParameter))
925
+ return false;
926
+ const constraint = this.checker.getBaseConstraintOfType(type);
927
+ return constraint === undefined || this.isOpenTop(constraint);
928
+ }
929
+ isEmptyObject(type) {
930
+ return ((type.flags & ts.TypeFlags.Object) !== 0 &&
931
+ this.declaredProperties(type).length === 0 &&
932
+ type.getCallSignatures().length === 0 &&
933
+ type.getConstructSignatures().length === 0 &&
934
+ this.checker.getIndexInfosOfType(type).length === 0);
935
+ }
936
+ /** The type's parts besides null and undefined. */
937
+ parts(type) {
938
+ return (type.isUnion() ? type.types : [type]).filter(part => !isNullish(part));
939
+ }
940
+ /**
941
+ * What the signature has at parameter `index`, reading through a rest
942
+ * parameter; `undefined` when it has none there. A rest typed by a type
943
+ * parameter (`...rest: A`) is unreadable, unless `readConstraint` asks for
944
+ * its element through the constraint (`A extends Array<X>` reads `X`).
945
+ */
946
+ parameterAt(signature, index, readConstraint = false) {
947
+ const parameters = signature.getParameters();
948
+ const last = parameters[parameters.length - 1];
949
+ const declaration = last?.valueDeclaration;
950
+ const restIndex = declaration && ts.isParameter(declaration) && declaration.dotDotDotToken
951
+ ? parameters.length - 1
952
+ : -1;
953
+ if (restIndex === -1 || index < restIndex) {
954
+ return index < parameters.length ? this.checker.getTypeOfSymbol(parameters[index]) : undefined;
955
+ }
956
+ let rest = this.checker.getTypeOfSymbol(last);
957
+ if (readConstraint && rest.flags & ts.TypeFlags.TypeParameter) {
958
+ rest = this.checker.getBaseConstraintOfType(rest) ?? rest;
959
+ }
960
+ if (this.checker.isTupleType(rest)) {
961
+ return this.checker.getTypeArguments(rest)[index - restIndex];
962
+ }
963
+ if (this.checker.isArrayType(rest)) {
964
+ return this.checker.getTypeArguments(rest)[0];
965
+ }
966
+ return this.isOpenTop(rest) ? rest : VARIADIC;
967
+ }
968
+ }
969
+ /**
970
+ * The package a specifier names (`@scope/name` or `name`, without a subpath)
971
+ * is `packageName`, or `packageName` is its `@types` package
972
+ * (`@types/scope__name` for a scoped one).
973
+ */
974
+ function isNamedPackage(specifier, packageName) {
975
+ const named = packageNameOf(specifier);
976
+ const types = `@types/${named.startsWith('@') ? named.slice(1).replace('/', '__') : named}`;
977
+ return packageName === named || packageName === types;
978
+ }
979
+ /** The package a specifier names: `@scope/name` or `name`, without a subpath. */
980
+ function packageNameOf(specifier) {
981
+ const segments = specifier.split('/');
982
+ return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0];
983
+ }
984
+ function readInstalledPackage(host, directory, directoryName) {
985
+ const manifest = path.join(directory, 'package.json');
986
+ if (!host.fileExists(manifest))
987
+ return null;
988
+ let packageName;
989
+ try {
990
+ const parsed = JSON.parse(host.readFile(manifest) ?? '');
991
+ const name = parsed?.name;
992
+ if (typeof name === 'string')
993
+ packageName = name;
994
+ }
995
+ catch {
996
+ // An unreadable manifest still marks an installed directory; it names no package.
997
+ }
998
+ return { directoryName, packageName };
999
+ }
1000
+ function isNullish(type) {
1001
+ return (type.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0;
1002
+ }
1003
+ /** `string`, a string literal or template, or an intersection with one (`string & {}`). */
1004
+ function isStringLike(type) {
1005
+ if (type.flags & ts.TypeFlags.StringLike)
1006
+ return true;
1007
+ return type.isIntersection() && type.types.some(isStringLike);
1008
+ }