carrick 0.3.97 → 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.
- package/bin/carrick.mjs +19 -14
- package/dist/auth/run.d.ts +1 -1
- package/dist/auth/run.js +1 -1
- package/dist/contract.d.ts +15 -0
- package/dist/contract.js.map +1 -1
- package/dist/global-install.js +6 -5
- package/dist/global-install.js.map +1 -1
- package/dist/hook/reuse.d.ts +8 -3
- package/dist/hook/reuse.js +15 -3
- package/dist/hook/reuse.js.map +1 -1
- package/dist/init/connect.js +1 -1
- package/dist/init/connect.js.map +1 -1
- package/dist/init/doctor.js +5 -2
- package/dist/init/doctor.js.map +1 -1
- package/dist/init/hosted.d.ts +18 -1
- package/dist/init/hosted.js +84 -16
- package/dist/init/hosted.js.map +1 -1
- package/dist/init/install-id.d.ts +2 -2
- package/dist/init/install-id.js +3 -3
- package/dist/init/install-id.js.map +1 -1
- package/dist/init/mcp.d.ts +2 -2
- package/dist/init/mcp.js +2 -2
- package/dist/init/mcp.js.map +1 -1
- package/dist/init/outdated.d.ts +1 -1
- package/dist/init/outdated.js +1 -1
- package/dist/init/outdated.js.map +1 -1
- package/dist/init/output.d.ts +18 -1
- package/dist/init/output.js +40 -1
- package/dist/init/output.js.map +1 -1
- package/dist/init/remove.d.ts +71 -2
- package/dist/init/remove.js +474 -145
- package/dist/init/remove.js.map +1 -1
- package/dist/init/repo-copies.d.ts +16 -4
- package/dist/init/repo-copies.js +20 -6
- package/dist/init/repo-copies.js.map +1 -1
- package/dist/init/run.d.ts +12 -16
- package/dist/init/run.js +25 -42
- package/dist/init/run.js.map +1 -1
- package/dist/update.js +1 -0
- package/dist/update.js.map +1 -1
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/client-semantics.d.ts +36 -0
- package/sidecar/dist/src/client-semantics.js +1008 -0
- package/sidecar/dist/src/index.js +35 -0
- package/sidecar/dist/src/types.d.ts +83 -2
- package/sidecar/dist/src/validators.d.ts +463 -0
- 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
|
+
}
|