carrick 0.3.100 → 0.3.101
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/dist/scan.d.ts +11 -0
- package/dist/scan.js +15 -0
- package/dist/scan.js.map +1 -1
- package/package.json +6 -6
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/sidecar/dist/src/index.js +64 -10
- package/sidecar/dist/src/library-claims.d.ts +105 -0
- package/sidecar/dist/src/library-claims.js +2840 -0
- package/sidecar/dist/src/types.d.ts +224 -2
- package/sidecar/dist/src/validators.d.ts +1608 -18
- package/sidecar/dist/src/validators.js +116 -0
- package/sidecar/dist/src/client-semantics.d.ts +0 -36
- package/sidecar/dist/src/client-semantics.js +0 -1008
|
@@ -0,0 +1,2840 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `verify_library_claims` (carrick#1616) and `verify_client_semantics`
|
|
3
|
+
* (carrick#1564): check what a model said about a library against the
|
|
4
|
+
* package's own type declarations.
|
|
5
|
+
*
|
|
6
|
+
* A claim says what one export of a package does (its role, from a closed
|
|
7
|
+
* list) and where each part of a call through it sits: which member makes an
|
|
8
|
+
* instance and which option keys it reads, which member acts on the wire,
|
|
9
|
+
* which argument or key holds the name (a path, topic or event), the payload,
|
|
10
|
+
* the handler. The scanner reads call sites through a claim only when this
|
|
11
|
+
* check verified it, and a verified claim becomes a fact that can fail a pull
|
|
12
|
+
* request check. So a claim is `verified` only when the declarations say so
|
|
13
|
+
* positively. `failed` means the declarations resolved and contradict the
|
|
14
|
+
* claim; `unchecked` means they could not be read, or say nothing at the place
|
|
15
|
+
* the claim needs (`any`, `unknown`, `{}`, an unconstrained type parameter).
|
|
16
|
+
* The scanner drops both.
|
|
17
|
+
*
|
|
18
|
+
* One probe file, one set of resolution rules and one set of definitions
|
|
19
|
+
* ("declared", "accepts string", "says nothing") serve every role. The role
|
|
20
|
+
* alone picks which checks a claim needs (`ROLE_TABLE`): `http_client` keeps
|
|
21
|
+
* the #1564 checks and reasons exactly, so `verify_client_semantics` converts
|
|
22
|
+
* its checks into the shared shape (`httpCheck`) and answers through here;
|
|
23
|
+
* `broker`, `in_process_bus` and `socket` take the message checks, which add
|
|
24
|
+
* the contract's must-not-verify rules (a member only another package
|
|
25
|
+
* declares, a name slot beside a string slot the claim does not account for,
|
|
26
|
+
* a handler that says nothing). The wire is pinned on carrick#1564 (comment
|
|
27
|
+
* 5937606126, section 3), as contract amendment 2 (comment 5939543981)
|
|
28
|
+
* changes it: a maker's parts are slots, `of` is a receiver id, and a
|
|
29
|
+
* receiver a generic maker or scope builds is read at its declared
|
|
30
|
+
* type-parameter defaults.
|
|
31
|
+
*
|
|
32
|
+
* The export's type is read the way the service's own code would read it: a
|
|
33
|
+
* probe file in `from_dir` imports it, inside the service's program, under the
|
|
34
|
+
* service's compiler options. The declarations must be the installed
|
|
35
|
+
* package's: a `paths` alias or a `declare module` block the service writes
|
|
36
|
+
* for itself is not evidence about the library.
|
|
37
|
+
*/
|
|
38
|
+
import * as crypto from 'node:crypto';
|
|
39
|
+
import * as fs from 'node:fs';
|
|
40
|
+
import * as path from 'node:path';
|
|
41
|
+
import { ts } from 'ts-morph';
|
|
42
|
+
/** The methods a `verb` claim may name, and a method key may accept. */
|
|
43
|
+
const HTTP_METHODS = new Set([
|
|
44
|
+
'GET',
|
|
45
|
+
'POST',
|
|
46
|
+
'PUT',
|
|
47
|
+
'PATCH',
|
|
48
|
+
'DELETE',
|
|
49
|
+
'HEAD',
|
|
50
|
+
'OPTIONS',
|
|
51
|
+
]);
|
|
52
|
+
/**
|
|
53
|
+
* Interfaces whose members every value of that kind inherits. A key found only
|
|
54
|
+
* on one of these (`constructor`, `toString`, a string's `length`) is not a
|
|
55
|
+
* key the library declares.
|
|
56
|
+
*/
|
|
57
|
+
const BUILTIN_INTERFACES = new Set([
|
|
58
|
+
'Object',
|
|
59
|
+
'Function',
|
|
60
|
+
'CallableFunction',
|
|
61
|
+
'NewableFunction',
|
|
62
|
+
'String',
|
|
63
|
+
'Number',
|
|
64
|
+
'Boolean',
|
|
65
|
+
'Symbol',
|
|
66
|
+
'BigInt',
|
|
67
|
+
'Array',
|
|
68
|
+
'ReadonlyArray',
|
|
69
|
+
]);
|
|
70
|
+
/**
|
|
71
|
+
* The runtime's own type packages. A type they declare (the runtime's event
|
|
72
|
+
* emitter above all) is never part of another package's own declarations,
|
|
73
|
+
* even when that package's maker returns it: a member only the runtime
|
|
74
|
+
* declares is the runtime's, not the package's. When the named package is
|
|
75
|
+
* one of these, it is its own.
|
|
76
|
+
*/
|
|
77
|
+
const RUNTIME_TYPE_PACKAGES = new Set([
|
|
78
|
+
'@types/node',
|
|
79
|
+
'@types/bun',
|
|
80
|
+
'bun-types',
|
|
81
|
+
'@types/deno',
|
|
82
|
+
]);
|
|
83
|
+
/**
|
|
84
|
+
* A runtime module's own specifier (`node:events`). Its declarations are the
|
|
85
|
+
* runtime's types package's, which is therefore its home (contract
|
|
86
|
+
* amendment 1, A1, on carrick#1564). The bare name (`events`) is a registry
|
|
87
|
+
* package's, never the runtime module.
|
|
88
|
+
*/
|
|
89
|
+
const RUNTIME_MODULE = /^node:/;
|
|
90
|
+
/** A dependency range that names the service's own source, not a registry release. */
|
|
91
|
+
const LOCAL_RANGE = /^(workspace|file|link|portal):/;
|
|
92
|
+
/** A resolved module lands on TypeScript: a declaration file or source. */
|
|
93
|
+
const TYPESCRIPT_FILE = /\.(d\.[mc]?ts|[mc]?tsx?)$/;
|
|
94
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
95
|
+
/** The HTTP receivers of #1564: the export, or what one factory member returns. */
|
|
96
|
+
const HTTP_RECEIVER = /^(export|instance:\S+)$/;
|
|
97
|
+
const HTTP_RULES = { http: true, message: false, ops: new Set(['request']) };
|
|
98
|
+
const MESSAGE_RULES = {
|
|
99
|
+
http: false,
|
|
100
|
+
message: true,
|
|
101
|
+
ops: new Set(['send', 'receive', 'request']),
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The role table: the role alone picks the checks. A role this build does
|
|
105
|
+
* not read yet (GraphQL executors, server mounts) answers `role_unsupported`,
|
|
106
|
+
* and `none` has nothing on the wire to check.
|
|
107
|
+
*/
|
|
108
|
+
const ROLE_TABLE = {
|
|
109
|
+
http_client: HTTP_RULES,
|
|
110
|
+
broker: MESSAGE_RULES,
|
|
111
|
+
in_process_bus: MESSAGE_RULES,
|
|
112
|
+
socket: MESSAGE_RULES,
|
|
113
|
+
graphql_client: undefined,
|
|
114
|
+
server_framework: undefined,
|
|
115
|
+
none: undefined,
|
|
116
|
+
};
|
|
117
|
+
const VERIFIED = { verdict: 'verified' };
|
|
118
|
+
const failed = (reason) => ({ verdict: 'failed', reason });
|
|
119
|
+
const unchecked = (reason) => ({ verdict: 'unchecked', reason });
|
|
120
|
+
/**
|
|
121
|
+
* A rest parameter typed by a type variable (`...rest: A`): the element at a
|
|
122
|
+
* position is `A[number]`, which says nothing the predicates can read.
|
|
123
|
+
*/
|
|
124
|
+
const VARIADIC = Symbol('variadic element');
|
|
125
|
+
function furthest(current, rank, outcome) {
|
|
126
|
+
if (!current || rank > current.rank)
|
|
127
|
+
return { rank, outcome };
|
|
128
|
+
if (rank === current.rank && current.outcome.verdict === 'failed' && outcome.verdict === 'unchecked') {
|
|
129
|
+
return { rank, outcome };
|
|
130
|
+
}
|
|
131
|
+
return current;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* A #1564 HTTP check in the shared shape. The conversion is one to one, so
|
|
135
|
+
* each claim keeps its own id and its own verdict:
|
|
136
|
+
* - `factory` is a `make` call of `member` whose base is the base key of the
|
|
137
|
+
* options at argument 0;
|
|
138
|
+
* - `verb` is a `request` op with a fixed method and the path at argument 0;
|
|
139
|
+
* - `verb_body` is the same op with the body at argument 1, or with an
|
|
140
|
+
* options object at argument 1 and the body as one of its keys;
|
|
141
|
+
* - `request` is a `request` op whose url and method keys sit on the config
|
|
142
|
+
* at argument 0, or whose path is argument 0 and method key sits on the
|
|
143
|
+
* options at argument 1; `request_body` adds the body key there.
|
|
144
|
+
*/
|
|
145
|
+
export function httpCheck(check) {
|
|
146
|
+
const common = {
|
|
147
|
+
claim_id: check.claim_id,
|
|
148
|
+
package: check.package,
|
|
149
|
+
export: check.export,
|
|
150
|
+
role: 'http_client',
|
|
151
|
+
receiver: check.receiver,
|
|
152
|
+
};
|
|
153
|
+
const claim = check.claim;
|
|
154
|
+
switch (claim.kind) {
|
|
155
|
+
case 'factory':
|
|
156
|
+
return {
|
|
157
|
+
...common,
|
|
158
|
+
claim: { kind: 'make', form: 'call', member: claim.member, base: { arg: 0, key: claim.base_url_key } },
|
|
159
|
+
};
|
|
160
|
+
case 'verb':
|
|
161
|
+
return {
|
|
162
|
+
...common,
|
|
163
|
+
claim: { kind: 'op', op: 'request', member: claim.member, method: claim.method, name: { arg: 0 } },
|
|
164
|
+
};
|
|
165
|
+
case 'verb_body':
|
|
166
|
+
return {
|
|
167
|
+
...common,
|
|
168
|
+
claim: claim.args === 'path_body'
|
|
169
|
+
? { kind: 'op', op: 'request', member: claim.member, name: { arg: 0 }, payload: { arg: 1 } }
|
|
170
|
+
: {
|
|
171
|
+
kind: 'op',
|
|
172
|
+
op: 'request',
|
|
173
|
+
member: claim.member,
|
|
174
|
+
name: { arg: 0 },
|
|
175
|
+
options: { arg: 1 },
|
|
176
|
+
...(claim.body_key === undefined ? {} : { payload: { arg: 1, key: claim.body_key } }),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
case 'request':
|
|
180
|
+
case 'request_body': {
|
|
181
|
+
const at = claim.args === 'config' ? 0 : 1;
|
|
182
|
+
const name = claim.args === 'config'
|
|
183
|
+
? claim.url_key === undefined
|
|
184
|
+
? undefined
|
|
185
|
+
: { arg: 0, key: claim.url_key }
|
|
186
|
+
: { arg: 0 };
|
|
187
|
+
return {
|
|
188
|
+
...common,
|
|
189
|
+
claim: {
|
|
190
|
+
kind: 'op',
|
|
191
|
+
op: 'request',
|
|
192
|
+
member: claim.member,
|
|
193
|
+
...(name === undefined ? {} : { name }),
|
|
194
|
+
method_key: { arg: at, key: claim.method_key },
|
|
195
|
+
...(claim.kind === 'request_body' ? { payload: { arg: at, key: claim.body_key } } : {}),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
let probeSequence = 0;
|
|
202
|
+
export class LibraryClaimsVerifier {
|
|
203
|
+
project;
|
|
204
|
+
constructor(project) {
|
|
205
|
+
this.project = project;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Judge every check, in request order, spending at most `budgetMs`. Checks
|
|
209
|
+
* the budget does not reach come back `unchecked` with reason `budget`.
|
|
210
|
+
* A check on an instance or a scope reads the maker or scope claims of the
|
|
211
|
+
* same request first, whatever their place in it.
|
|
212
|
+
*/
|
|
213
|
+
run(fromDir, checks, budgetMs) {
|
|
214
|
+
const deadline = performance.now() + budgetMs;
|
|
215
|
+
const stamp = (check, outcome) => outcome.verdict === 'verified'
|
|
216
|
+
? { claim_id: check.claim_id, receiver: check.receiver, verdict: 'verified' }
|
|
217
|
+
: {
|
|
218
|
+
claim_id: check.claim_id,
|
|
219
|
+
receiver: check.receiver,
|
|
220
|
+
verdict: outcome.verdict,
|
|
221
|
+
reason: outcome.reason,
|
|
222
|
+
};
|
|
223
|
+
if (checks.length === 0)
|
|
224
|
+
return { semantics: [], modules: [] };
|
|
225
|
+
// One import line per distinct (package, export), in first-seen order.
|
|
226
|
+
const importKeys = [];
|
|
227
|
+
const importIndex = new Map();
|
|
228
|
+
// HTTP: the base-URL keys the request's factory claims name, per factory:
|
|
229
|
+
// an instance is read through the overload that declares them.
|
|
230
|
+
const factoryKeys = new Map();
|
|
231
|
+
// The roles each (package, export) is given across the request.
|
|
232
|
+
const roles = new Map();
|
|
233
|
+
for (const check of checks) {
|
|
234
|
+
const key = exportKey(check);
|
|
235
|
+
if (!importIndex.has(key)) {
|
|
236
|
+
importIndex.set(key, importKeys.length);
|
|
237
|
+
importKeys.push(key);
|
|
238
|
+
}
|
|
239
|
+
const given = roles.get(key) ?? new Set();
|
|
240
|
+
given.add(check.role);
|
|
241
|
+
roles.set(key, given);
|
|
242
|
+
const claim = check.claim;
|
|
243
|
+
const baseKey = claim.kind === 'make' && claim.base?.arg === 0 ? claim.base.key : undefined;
|
|
244
|
+
if (check.role === 'http_client' && claim.kind === 'make' && claim.member !== null && baseKey !== undefined) {
|
|
245
|
+
const factory = factoryKey(key, claim.member);
|
|
246
|
+
const keys = factoryKeys.get(factory) ?? new Set();
|
|
247
|
+
keys.add(baseKey);
|
|
248
|
+
factoryKeys.set(factory, keys);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const importLines = importKeys.map((key, i) => {
|
|
252
|
+
const [pkg, name] = JSON.parse(key);
|
|
253
|
+
return importLine(pkg, name, `__carrick_e${i}`);
|
|
254
|
+
});
|
|
255
|
+
// One statement per receiver a message check's maker or scope claim
|
|
256
|
+
// builds, after the imports: that receiver built with no argument and no
|
|
257
|
+
// type argument. Its resolved signature is the maker (or scope) read at
|
|
258
|
+
// its declared type-parameter defaults (`DeclarationReader.returnOf`). An
|
|
259
|
+
// HTTP request adds none, so its probe is the one #1564 reads.
|
|
260
|
+
const builtLines = [];
|
|
261
|
+
const builtIndex = new Map();
|
|
262
|
+
for (const check of checks) {
|
|
263
|
+
if (!ROLE_TABLE[check.role]?.message)
|
|
264
|
+
continue;
|
|
265
|
+
const built = builtReceiver(check);
|
|
266
|
+
if (built === undefined)
|
|
267
|
+
continue;
|
|
268
|
+
const key = builtKey(check, built);
|
|
269
|
+
if (builtIndex.has(key))
|
|
270
|
+
continue;
|
|
271
|
+
const expression = receiverExpression(`__carrick_e${importIndex.get(exportKey(check))}`, built);
|
|
272
|
+
if (expression === undefined)
|
|
273
|
+
continue;
|
|
274
|
+
builtIndex.set(key, importLines.length + builtLines.length);
|
|
275
|
+
builtLines.push(`${expression};`);
|
|
276
|
+
}
|
|
277
|
+
const probeText = [...importLines, ...builtLines].join('\n');
|
|
278
|
+
const probePath = path.join(fromDir, `__carrick_claims_probe_${process.pid}_${probeSequence++}.ts`);
|
|
279
|
+
const probe = this.project.createSourceFile(probePath, `${probeText}\n`, { overwrite: true });
|
|
280
|
+
try {
|
|
281
|
+
const program = this.project.getProgram().compilerObject;
|
|
282
|
+
const checker = program.getTypeChecker();
|
|
283
|
+
const file = program.getSourceFile(probe.getFilePath());
|
|
284
|
+
if (!file)
|
|
285
|
+
throw new Error(`probe file ${probePath} is not in the program`);
|
|
286
|
+
const reader = new DeclarationReader(program, file, this.project.getModuleResolutionHost(), fromDir);
|
|
287
|
+
const localRanges = readLocalRanges(fromDir);
|
|
288
|
+
const moduleReads = new Map();
|
|
289
|
+
const exportReads = new Map();
|
|
290
|
+
const httpReceivers = new Map();
|
|
291
|
+
const judged = new Map();
|
|
292
|
+
const declarationOf = (check) => file.statements[importIndex.get(exportKey(check))];
|
|
293
|
+
/** The probe's no-argument build of `receiver` on the check's export, resolved. */
|
|
294
|
+
const atDefaults = (check, receiver) => {
|
|
295
|
+
const index = builtIndex.get(builtKey(check, receiver));
|
|
296
|
+
return index === undefined ? undefined : resolvedBuild(checker, file.statements[index]);
|
|
297
|
+
};
|
|
298
|
+
// A runtime module is read for the message roles only: HTTP answers as #1564 does.
|
|
299
|
+
const readModule = (check, runtimeModules) => {
|
|
300
|
+
const key = `${runtimeModules}\u0000${check.package}`;
|
|
301
|
+
let moduleRead = moduleReads.get(key);
|
|
302
|
+
if (!moduleRead) {
|
|
303
|
+
moduleRead = reader.readModule(check.package, declarationOf(check), runtimeModules);
|
|
304
|
+
moduleReads.set(key, moduleRead);
|
|
305
|
+
}
|
|
306
|
+
return moduleRead;
|
|
307
|
+
};
|
|
308
|
+
const readExport = (check) => {
|
|
309
|
+
const key = exportKey(check);
|
|
310
|
+
let exportRead = exportReads.get(key);
|
|
311
|
+
if (!exportRead) {
|
|
312
|
+
exportRead = reader.readExport(declarationOf(check));
|
|
313
|
+
exportReads.set(key, exportRead);
|
|
314
|
+
}
|
|
315
|
+
return exportRead;
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* The message receivers of check `index`, reading its maker and scope
|
|
319
|
+
* claims: one per distinct type the holding overloads return. A maker
|
|
320
|
+
* whose two overloads both hold builds either instance, so a claim on
|
|
321
|
+
* it must hold on each.
|
|
322
|
+
*/
|
|
323
|
+
const readMessageReceivers = (index, exported) => {
|
|
324
|
+
const check = checks[index];
|
|
325
|
+
// `fitsReceiver` admitted it.
|
|
326
|
+
const receiverPath = parseReceiver(check.receiver);
|
|
327
|
+
let types = [exported.type];
|
|
328
|
+
let makerBindsName = false;
|
|
329
|
+
if (receiverPath.maker) {
|
|
330
|
+
const maker = receiverPath.maker;
|
|
331
|
+
const makers = checks
|
|
332
|
+
.map((other, i) => ({ other, i }))
|
|
333
|
+
.filter(({ other }) => other.package === check.package &&
|
|
334
|
+
other.export === check.export &&
|
|
335
|
+
other.receiver === 'export' &&
|
|
336
|
+
other.claim.kind === 'make' &&
|
|
337
|
+
other.claim.form === maker.form &&
|
|
338
|
+
other.claim.member === maker.member);
|
|
339
|
+
const made = holdingReturns(makers.map(({ i }) => judge(i)), atDefaults(check, receiverPath.base));
|
|
340
|
+
if ('reason' in made)
|
|
341
|
+
return { reason: made.reason === 'unresolved' ? 'maker_unresolved' : 'maker_unverified' };
|
|
342
|
+
types = made.value;
|
|
343
|
+
makerBindsName = makers.some(({ other }) => other.claim.kind === 'make' && other.claim.name !== undefined);
|
|
344
|
+
}
|
|
345
|
+
if (receiverPath.scope !== undefined) {
|
|
346
|
+
const scope = receiverPath.scope;
|
|
347
|
+
const scopes = checks
|
|
348
|
+
.map((other, i) => ({ other, i }))
|
|
349
|
+
.filter(({ other }) => other.package === check.package &&
|
|
350
|
+
other.export === check.export &&
|
|
351
|
+
other.receiver === receiverPath.base &&
|
|
352
|
+
other.claim.kind === 'scope' &&
|
|
353
|
+
scopeStep(other.claim) === scope);
|
|
354
|
+
const scoped = holdingReturns(scopes.map(({ i }) => judge(i)), atDefaults(check, check.receiver));
|
|
355
|
+
if ('reason' in scoped)
|
|
356
|
+
return { reason: scoped.reason === 'unresolved' ? 'scope_unresolved' : 'scope_unverified' };
|
|
357
|
+
types = scoped.value;
|
|
358
|
+
}
|
|
359
|
+
if (types.some(type => reader.returnSaysNothing(type))) {
|
|
360
|
+
return { reason: receiverPath.maker ? 'maker_unresolved' : 'export_untyped' };
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
value: types.map(type => ({
|
|
364
|
+
type,
|
|
365
|
+
pkg: check.package,
|
|
366
|
+
home: reader.homeOf(check.package, [exported.target, ...typeSymbols(type)]),
|
|
367
|
+
makerBindsName,
|
|
368
|
+
scoped: receiverPath.scope !== undefined,
|
|
369
|
+
})),
|
|
370
|
+
};
|
|
371
|
+
};
|
|
372
|
+
/**
|
|
373
|
+
* What a set of maker (or scope) judgements build: the distinct types
|
|
374
|
+
* their common overloads return, each read at its declared
|
|
375
|
+
* type-parameter defaults where `built` (the probe's no-argument build
|
|
376
|
+
* of that receiver) instantiates it.
|
|
377
|
+
*/
|
|
378
|
+
const holdingReturns = (judgements, built) => {
|
|
379
|
+
if (judgements.length === 0)
|
|
380
|
+
return { reason: 'unverified' };
|
|
381
|
+
let holding;
|
|
382
|
+
for (const judgement of judgements) {
|
|
383
|
+
if (judgement.outcome.verdict !== 'verified' || !judgement.holding)
|
|
384
|
+
return { reason: 'unverified' };
|
|
385
|
+
holding = holding === undefined ? judgement.holding : holding.filter(sig => judgement.holding.includes(sig));
|
|
386
|
+
}
|
|
387
|
+
const returns = [...new Set((holding ?? []).map(sig => reader.returnOf(sig, built)))];
|
|
388
|
+
if (returns.length === 0)
|
|
389
|
+
return { reason: 'unresolved' };
|
|
390
|
+
return { value: returns };
|
|
391
|
+
};
|
|
392
|
+
const judge = (index) => {
|
|
393
|
+
const cached = judged.get(index);
|
|
394
|
+
if (cached)
|
|
395
|
+
return cached;
|
|
396
|
+
// A maker read on its own instance, or a scope on itself, is a cycle.
|
|
397
|
+
judged.set(index, { outcome: unchecked('receiver_invalid') });
|
|
398
|
+
const result = judgeCheck(index);
|
|
399
|
+
judged.set(index, result);
|
|
400
|
+
return result;
|
|
401
|
+
};
|
|
402
|
+
const judgeCheck = (index) => {
|
|
403
|
+
const check = checks[index];
|
|
404
|
+
const rules = ROLE_TABLE[check.role];
|
|
405
|
+
if (!rules)
|
|
406
|
+
return { outcome: unchecked('role_unsupported') };
|
|
407
|
+
// An export given two roles is classified two ways; neither is a fact.
|
|
408
|
+
if ((roles.get(exportKey(check))?.size ?? 0) > 1)
|
|
409
|
+
return { outcome: unchecked('role_conflict') };
|
|
410
|
+
if (placementInvalid(check.claim))
|
|
411
|
+
return { outcome: unchecked('claim_invalid') };
|
|
412
|
+
if (rules.http && !HTTP_RECEIVER.test(check.receiver))
|
|
413
|
+
return { outcome: unchecked('receiver_invalid') };
|
|
414
|
+
if (rules.message && !fitsReceiver(check))
|
|
415
|
+
return { outcome: unchecked('receiver_invalid') };
|
|
416
|
+
reader.setPackage(check.package);
|
|
417
|
+
const moduleRead = readModule(check, rules.message);
|
|
418
|
+
if (moduleRead.reason)
|
|
419
|
+
return { outcome: unchecked(moduleRead.reason) };
|
|
420
|
+
if (rules.message && LOCAL_RANGE.test(localRanges.get(packageNameOf(check.package)) ?? '')) {
|
|
421
|
+
return { outcome: unchecked('module_workspace') };
|
|
422
|
+
}
|
|
423
|
+
const exportRead = readExport(check);
|
|
424
|
+
if ('reason' in exportRead)
|
|
425
|
+
return { outcome: unchecked(exportRead.reason) };
|
|
426
|
+
if (rules.http) {
|
|
427
|
+
const key = exportKey(check);
|
|
428
|
+
const receiverKey = `${key}\u0000${check.receiver}`;
|
|
429
|
+
let receiverRead = httpReceivers.get(receiverKey);
|
|
430
|
+
if (!receiverRead) {
|
|
431
|
+
const factory = check.receiver.startsWith('instance:') ? check.receiver.slice('instance:'.length) : undefined;
|
|
432
|
+
receiverRead = reader.readReceiver(exportRead.value.type, factory, factory === undefined ? undefined : factoryKeys.get(factoryKey(key, factory)));
|
|
433
|
+
httpReceivers.set(receiverKey, receiverRead);
|
|
434
|
+
}
|
|
435
|
+
if ('reason' in receiverRead)
|
|
436
|
+
return { outcome: unchecked(receiverRead.reason) };
|
|
437
|
+
return { outcome: reader.judgeHttp(receiverRead.value, check.claim) };
|
|
438
|
+
}
|
|
439
|
+
if (check.claim.kind === 'op' && !rules.ops.has(check.claim.op)) {
|
|
440
|
+
return { outcome: unchecked('role_unsupported') };
|
|
441
|
+
}
|
|
442
|
+
const receivers = readMessageReceivers(index, exportRead.value);
|
|
443
|
+
if ('reason' in receivers)
|
|
444
|
+
return { outcome: unchecked(receivers.reason) };
|
|
445
|
+
const built = builtReceiver(check);
|
|
446
|
+
const builtAtDefaults = built === undefined ? undefined : atDefaults(check, built);
|
|
447
|
+
const holding = [];
|
|
448
|
+
for (const receiver of receivers.value) {
|
|
449
|
+
const result = reader.judgeMessage(receiver, check.claim, builtAtDefaults);
|
|
450
|
+
if (result.outcome.verdict !== 'verified')
|
|
451
|
+
return result;
|
|
452
|
+
holding.push(...(result.holding ?? []));
|
|
453
|
+
}
|
|
454
|
+
return { outcome: VERIFIED, holding };
|
|
455
|
+
};
|
|
456
|
+
const semantics = [];
|
|
457
|
+
for (let index = 0; index < checks.length; index++) {
|
|
458
|
+
const check = checks[index];
|
|
459
|
+
if (!judged.has(index) && performance.now() >= deadline) {
|
|
460
|
+
semantics.push(stamp(check, unchecked('budget')));
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
semantics.push(stamp(check, judge(index).outcome));
|
|
464
|
+
}
|
|
465
|
+
return {
|
|
466
|
+
semantics,
|
|
467
|
+
modules: [...moduleReads.values()].map(read => read.entry),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
this.project.removeSourceFile(probe);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Each specifier's declared surface, read with the verifier's predicates
|
|
476
|
+
* (carrick#1660): every value export (or only those `only` names for the
|
|
477
|
+
* specifier), the receivers a claim can be read on, in the verifier's
|
|
478
|
+
* receiver grammar, and each receiver's callable members with their
|
|
479
|
+
* parameter slots. A runtime module (`node:events`) is listed from the
|
|
480
|
+
* runtime's types package, as the message roles read it. Capped at
|
|
481
|
+
* `maxEntries` per specifier, with the count dropped: every export and
|
|
482
|
+
* receiver is listed before any member name, and every member name before
|
|
483
|
+
* any signature, so the cap cuts signatures first and exports last.
|
|
484
|
+
*
|
|
485
|
+
* A receiver a maker builds is listed as the verifier reads it: built with
|
|
486
|
+
* no argument and no type argument, so a generic maker's instance is read
|
|
487
|
+
* at its declared type-parameter defaults (carrick#1696). That takes two
|
|
488
|
+
* programs. The first reads which receivers each export can make; the
|
|
489
|
+
* second adds the verifier's own build statement for each
|
|
490
|
+
* (`receiverExpression` on the export's `importLine`, read through
|
|
491
|
+
* `DeclarationReader.returnOf`) and lists from it.
|
|
492
|
+
*
|
|
493
|
+
* `surface_sha256` is the full-surface hash (see `fullSurfaceSha256`).
|
|
494
|
+
*/
|
|
495
|
+
listSurface(fromDir, packages, maxEntries, only = {}) {
|
|
496
|
+
if (packages.length === 0)
|
|
497
|
+
return { surfaces: [], surface_sha256: fullSurfaceSha256([]) };
|
|
498
|
+
// Each specifier twice: as a namespace, which holds its named exports, and
|
|
499
|
+
// as a default import, which is how a module that exports a value whole
|
|
500
|
+
// (`export =`) is imported as `default`.
|
|
501
|
+
const imports = packages.flatMap((pkg, i) => [
|
|
502
|
+
`import * as __carrick_ns${i} from ${JSON.stringify(pkg)};`,
|
|
503
|
+
importLine(pkg, 'default', `__carrick_d${i}`),
|
|
504
|
+
]);
|
|
505
|
+
const probeOf = (file, i) => ({
|
|
506
|
+
namespace: file.statements[2 * i],
|
|
507
|
+
defaultImport: file.statements[2 * i + 1],
|
|
508
|
+
});
|
|
509
|
+
const makers = this.readProbe(fromDir, imports, (_checker, reader, file) => packages.map((pkg, i) => reader.makersOf(pkg, probeOf(file, i), only[pkg])));
|
|
510
|
+
const importLines = [];
|
|
511
|
+
const buildLines = [];
|
|
512
|
+
const builtIndex = new Map();
|
|
513
|
+
packages.forEach((pkg, i) => {
|
|
514
|
+
for (const { export: name, receivers } of makers[i]) {
|
|
515
|
+
const local = `__carrick_e${importLines.length}`;
|
|
516
|
+
importLines.push(importLine(pkg, name, local));
|
|
517
|
+
for (const receiver of receivers) {
|
|
518
|
+
const expression = receiverExpression(local, receiver);
|
|
519
|
+
if (expression === undefined)
|
|
520
|
+
continue;
|
|
521
|
+
builtIndex.set(builtKey({ package: pkg, export: name }, receiver), buildLines.length);
|
|
522
|
+
buildLines.push(`${expression};`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
const firstBuild = imports.length + importLines.length;
|
|
527
|
+
const surfaces = this.readProbe(fromDir, [...imports, ...importLines, ...buildLines], (checker, reader, file) => packages.map((pkg, i) => reader.listPackage(pkg, probeOf(file, i), maxEntries, only[pkg], (name, receiver) => {
|
|
528
|
+
const index = builtIndex.get(builtKey({ package: pkg, export: name }, receiver));
|
|
529
|
+
return index === undefined ? undefined : resolvedBuild(checker, file.statements[firstBuild + index]);
|
|
530
|
+
})));
|
|
531
|
+
return { surfaces, surface_sha256: fullSurfaceSha256(surfaces) };
|
|
532
|
+
}
|
|
533
|
+
/** `read` over a probe file holding `lines` in `fromDir`, removed again afterwards. */
|
|
534
|
+
readProbe(fromDir, lines, read) {
|
|
535
|
+
const probePath = path.join(fromDir, `__carrick_surface_probe_${process.pid}_${probeSequence++}.ts`);
|
|
536
|
+
const probe = this.project.createSourceFile(probePath, `${lines.join('\n')}\n`, { overwrite: true });
|
|
537
|
+
try {
|
|
538
|
+
const program = this.project.getProgram().compilerObject;
|
|
539
|
+
const file = program.getSourceFile(probe.getFilePath());
|
|
540
|
+
if (!file)
|
|
541
|
+
throw new Error(`probe file ${probePath} is not in the program`);
|
|
542
|
+
const reader = new DeclarationReader(program, file, this.project.getModuleResolutionHost(), fromDir);
|
|
543
|
+
return read(program.getTypeChecker(), reader, file);
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
this.project.removeSourceFile(probe);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* What a probe statement that builds a receiver (`receiverExpression`)
|
|
552
|
+
* resolves to: its maker or scope signature as a call with no argument and
|
|
553
|
+
* no type argument instantiates it (see `DeclarationReader.returnOf`).
|
|
554
|
+
*/
|
|
555
|
+
function resolvedBuild(checker, statement) {
|
|
556
|
+
if (!statement || !ts.isExpressionStatement(statement))
|
|
557
|
+
return undefined;
|
|
558
|
+
const expression = statement.expression;
|
|
559
|
+
if (!ts.isCallExpression(expression) && !ts.isNewExpression(expression))
|
|
560
|
+
return undefined;
|
|
561
|
+
return checker.getResolvedSignature(expression);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* The full-surface hash the shared library store keys on: sha256 of the JSON
|
|
565
|
+
* array of `[package, exports]` for every surface that listed at least one
|
|
566
|
+
* export, sorted by `package` (code unit order). A specifier that lists
|
|
567
|
+
* nothing (unresolved, local, no value exports) is left out, so asking for a
|
|
568
|
+
* subpath a version does not have changes nothing. Type text already names
|
|
569
|
+
* the request's directory as `<root>`.
|
|
570
|
+
*/
|
|
571
|
+
export function fullSurfaceSha256(surfaces) {
|
|
572
|
+
const listed = surfaces
|
|
573
|
+
.filter(surface => surface.reason === undefined && surface.exports.length > 0)
|
|
574
|
+
.map(surface => [surface.package, surface.exports])
|
|
575
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
576
|
+
return crypto.createHash('sha256').update(JSON.stringify(listed)).digest('hex');
|
|
577
|
+
}
|
|
578
|
+
function exportKey(check) {
|
|
579
|
+
return JSON.stringify([check.package, check.export]);
|
|
580
|
+
}
|
|
581
|
+
function factoryKey(exportKeyText, member) {
|
|
582
|
+
return `${exportKeyText}\u0000${member}`;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* `export`, `instance:()`, `instance:new`, `instance:new:<member>` or
|
|
586
|
+
* `instance:<member>`, optionally followed by `>scope:<path.member>`.
|
|
587
|
+
*/
|
|
588
|
+
function parseReceiver(text) {
|
|
589
|
+
const steps = text.split('>');
|
|
590
|
+
if (steps.length > 2)
|
|
591
|
+
return undefined;
|
|
592
|
+
const [base, scopeStep] = steps;
|
|
593
|
+
let scope;
|
|
594
|
+
if (scopeStep !== undefined) {
|
|
595
|
+
const match = /^scope:(\S+)$/.exec(scopeStep);
|
|
596
|
+
if (!match)
|
|
597
|
+
return undefined;
|
|
598
|
+
scope = match[1];
|
|
599
|
+
}
|
|
600
|
+
if (base === 'export')
|
|
601
|
+
return { base, scope };
|
|
602
|
+
if (base === 'instance:()')
|
|
603
|
+
return { base, scope, maker: { form: 'call', member: null } };
|
|
604
|
+
if (base === 'instance:new')
|
|
605
|
+
return { base, scope, maker: { form: 'new', member: null } };
|
|
606
|
+
let match = /^instance:new:(\S+)$/.exec(base);
|
|
607
|
+
if (match)
|
|
608
|
+
return { base, scope, maker: { form: 'new', member: match[1] } };
|
|
609
|
+
match = /^instance:(\S+)$/.exec(base);
|
|
610
|
+
if (match && match[1] !== '()' && match[1] !== 'new')
|
|
611
|
+
return { base, scope, maker: { form: 'call', member: match[1] } };
|
|
612
|
+
return undefined;
|
|
613
|
+
}
|
|
614
|
+
/** The scope step a scope claim's results are read through: its path and member, joined by `.`. */
|
|
615
|
+
function scopeStep(claim) {
|
|
616
|
+
return [...(claim.path ?? []), claim.member].join('.');
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* An op, scope or reserved name carries `on` or `of`, never both, and `of` is
|
|
620
|
+
* a receiver a maker or a scope builds, by its receiver id: never the export
|
|
621
|
+
* itself, which `on: 'export'` names (contract amendment 2, B3). "Both the
|
|
622
|
+
* export and one maker's instances" is two elements, not one.
|
|
623
|
+
*/
|
|
624
|
+
function placementInvalid(claim) {
|
|
625
|
+
if (claim.kind === 'make' || claim.of === undefined)
|
|
626
|
+
return false;
|
|
627
|
+
if (claim.on !== undefined)
|
|
628
|
+
return true;
|
|
629
|
+
const of = parseReceiver(claim.of);
|
|
630
|
+
return of === undefined || (of.maker === undefined && of.scope === undefined);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* A message check names a receiver its claim can be read on (contract
|
|
634
|
+
* amendment 2, B3). A maker is read on the export itself. An element with
|
|
635
|
+
* `of` is read on exactly that receiver. One with `on` is read on the export
|
|
636
|
+
* (`export`), on an instance of a maker (`instance`) or on either (`both`),
|
|
637
|
+
* and never on a receiver a scope returns, which only `of` names. One with
|
|
638
|
+
* neither names no receiver of its own. A claim read on a receiver it was
|
|
639
|
+
* not claimed for could verify a member of the same name with another shape
|
|
640
|
+
* (`client.write(name, value)` against `instance.write(value)`).
|
|
641
|
+
*/
|
|
642
|
+
function fitsReceiver(check) {
|
|
643
|
+
const receiver = parseReceiver(check.receiver);
|
|
644
|
+
if (!receiver)
|
|
645
|
+
return false;
|
|
646
|
+
const claim = check.claim;
|
|
647
|
+
if (claim.kind === 'make')
|
|
648
|
+
return check.receiver === 'export';
|
|
649
|
+
if (claim.of !== undefined)
|
|
650
|
+
return check.receiver === claim.of;
|
|
651
|
+
if (claim.on === undefined)
|
|
652
|
+
return true;
|
|
653
|
+
if (receiver.scope !== undefined)
|
|
654
|
+
return false;
|
|
655
|
+
if (claim.on === 'export')
|
|
656
|
+
return receiver.maker === undefined;
|
|
657
|
+
if (claim.on === 'instance')
|
|
658
|
+
return receiver.maker !== undefined;
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* The receiver a maker or scope check builds: what the maker makes
|
|
663
|
+
* (`instance:new`), or what the scope member returns on the receiver it is
|
|
664
|
+
* read on (`instance:connect>scope:channel`). Undefined for an op or a
|
|
665
|
+
* reserved name, and for a scope on a receiver a scope already returned.
|
|
666
|
+
*/
|
|
667
|
+
function builtReceiver(check) {
|
|
668
|
+
const claim = check.claim;
|
|
669
|
+
if (claim.kind === 'make') {
|
|
670
|
+
if (claim.member === null)
|
|
671
|
+
return claim.form === 'call' ? 'instance:()' : 'instance:new';
|
|
672
|
+
return claim.form === 'call' ? `instance:${claim.member}` : `instance:new:${claim.member}`;
|
|
673
|
+
}
|
|
674
|
+
if (claim.kind !== 'scope' || check.receiver.includes('>'))
|
|
675
|
+
return undefined;
|
|
676
|
+
return `${check.receiver}>scope:${scopeStep(claim)}`;
|
|
677
|
+
}
|
|
678
|
+
function builtKey(check, receiver) {
|
|
679
|
+
return JSON.stringify([check.package, check.export, receiver]);
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* What a service writes to build `receiver` on the export bound to `local`,
|
|
683
|
+
* passing no argument and no type argument: `new e()`, `e()`, `e.m()` or
|
|
684
|
+
* `new e.m()`, then `.path.member()` for a scope step. Undefined for a
|
|
685
|
+
* receiver id that does not parse.
|
|
686
|
+
*/
|
|
687
|
+
function receiverExpression(local, receiver) {
|
|
688
|
+
const parsed = parseReceiver(receiver);
|
|
689
|
+
if (!parsed)
|
|
690
|
+
return undefined;
|
|
691
|
+
const access = (name) => (IDENTIFIER.test(name) ? `.${name}` : `[${JSON.stringify(name)}]`);
|
|
692
|
+
let text = local;
|
|
693
|
+
if (parsed.maker) {
|
|
694
|
+
const callee = parsed.maker.member === null ? local : `${local}${access(parsed.maker.member)}`;
|
|
695
|
+
text = parsed.maker.form === 'new' ? `new ${callee}()` : `${callee}()`;
|
|
696
|
+
}
|
|
697
|
+
if (parsed.scope !== undefined)
|
|
698
|
+
text = `${text}${parsed.scope.split('.').map(access).join('')}()`;
|
|
699
|
+
return text;
|
|
700
|
+
}
|
|
701
|
+
/** The symbols that say who declared a type: its own, its alias's, its parts'. */
|
|
702
|
+
function typeSymbols(type) {
|
|
703
|
+
const symbols = [type.getSymbol(), type.aliasSymbol];
|
|
704
|
+
if (type.isUnionOrIntersection()) {
|
|
705
|
+
for (const part of type.types)
|
|
706
|
+
symbols.push(part.getSymbol(), part.aliasSymbol);
|
|
707
|
+
}
|
|
708
|
+
return symbols;
|
|
709
|
+
}
|
|
710
|
+
/** The probe's import of one export, bound to `local`. */
|
|
711
|
+
function importLine(pkg, name, local) {
|
|
712
|
+
const specifier = JSON.stringify(pkg);
|
|
713
|
+
if (name === 'default')
|
|
714
|
+
return `import ${local} from ${specifier};`;
|
|
715
|
+
const imported = IDENTIFIER.test(name) ? name : JSON.stringify(name);
|
|
716
|
+
return `import { ${imported} as ${local} } from ${specifier};`;
|
|
717
|
+
}
|
|
718
|
+
/** Upper-case ASCII letters only: `ſ` and `ı` must not become `S` and `I`. */
|
|
719
|
+
function asciiUpperCase(text) {
|
|
720
|
+
return text.replace(/[a-z]/g, letter => letter.toUpperCase());
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* The dependency ranges of the nearest package.json at or above `fromDir`, by
|
|
724
|
+
* package name: a `workspace:`, `file:`, `link:` or `portal:` range names the
|
|
725
|
+
* service's own source, which the verifier does not check yet (R2).
|
|
726
|
+
*/
|
|
727
|
+
function readLocalRanges(fromDir) {
|
|
728
|
+
const ranges = new Map();
|
|
729
|
+
let dir = path.resolve(fromDir);
|
|
730
|
+
for (;;) {
|
|
731
|
+
const manifest = path.join(dir, 'package.json');
|
|
732
|
+
if (fs.existsSync(manifest)) {
|
|
733
|
+
try {
|
|
734
|
+
const parsed = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
735
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
736
|
+
const deps = parsed[field];
|
|
737
|
+
if (!deps || typeof deps !== 'object')
|
|
738
|
+
continue;
|
|
739
|
+
for (const [name, range] of Object.entries(deps)) {
|
|
740
|
+
if (typeof range === 'string' && !ranges.has(name))
|
|
741
|
+
ranges.set(name, range);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
catch {
|
|
746
|
+
// An unreadable manifest names no ranges.
|
|
747
|
+
}
|
|
748
|
+
return ranges;
|
|
749
|
+
}
|
|
750
|
+
const parent = path.dirname(dir);
|
|
751
|
+
if (parent === dir)
|
|
752
|
+
return ranges;
|
|
753
|
+
dir = parent;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* Reads the declarations the probe imported, with the program's own checker.
|
|
758
|
+
* Every predicate is the contract's (carrick#1564, section 3), stated on the
|
|
759
|
+
* checker's public API, with the definitions the review amended.
|
|
760
|
+
*/
|
|
761
|
+
class DeclarationReader {
|
|
762
|
+
program;
|
|
763
|
+
probe;
|
|
764
|
+
host;
|
|
765
|
+
checker;
|
|
766
|
+
/** The service root, as a realpath. */
|
|
767
|
+
root;
|
|
768
|
+
packageDirectories = new Map();
|
|
769
|
+
realpaths = new Map();
|
|
770
|
+
/** Names the service declares in its own blocks, per augmented package (`global` for `declare global`). */
|
|
771
|
+
augmentedNames;
|
|
772
|
+
/** The package of the check being judged; see `serviceAugmentedNames`. */
|
|
773
|
+
currentPackage = '';
|
|
774
|
+
/** `declaredProperties` per type, for `currentPackage` (its augmentations decide them). */
|
|
775
|
+
declared = new Map();
|
|
776
|
+
/** The service root as given and as a realpath, longest first: what a listing prints as `<root>`. */
|
|
777
|
+
printedRoots;
|
|
778
|
+
constructor(program, probe, host, serviceRoot) {
|
|
779
|
+
this.program = program;
|
|
780
|
+
this.probe = probe;
|
|
781
|
+
this.host = host;
|
|
782
|
+
this.checker = program.getTypeChecker();
|
|
783
|
+
this.root = this.realpath(serviceRoot);
|
|
784
|
+
this.printedRoots = [...new Set([this.root, serviceRoot])]
|
|
785
|
+
.filter(root => root.length > 1)
|
|
786
|
+
.sort((a, b) => b.length - a.length);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* A path with its symlinks resolved. The compiler spells the service's own
|
|
790
|
+
* files as given and resolved dependencies as realpaths, so both sides of a
|
|
791
|
+
* comparison go through here. A path not on disk (the default library ts-morph
|
|
792
|
+
* serves from memory) is kept as it is.
|
|
793
|
+
*/
|
|
794
|
+
realpath(fileName) {
|
|
795
|
+
let real = this.realpaths.get(fileName);
|
|
796
|
+
if (real === undefined) {
|
|
797
|
+
try {
|
|
798
|
+
real = fs.realpathSync(fileName);
|
|
799
|
+
}
|
|
800
|
+
catch {
|
|
801
|
+
real = fileName;
|
|
802
|
+
}
|
|
803
|
+
this.realpaths.set(fileName, real);
|
|
804
|
+
}
|
|
805
|
+
return real;
|
|
806
|
+
}
|
|
807
|
+
// --------------------------------------------------------------------------
|
|
808
|
+
// Resolve, export, receiver
|
|
809
|
+
// --------------------------------------------------------------------------
|
|
810
|
+
/**
|
|
811
|
+
* Where the package resolved. The checker's module symbol is what the
|
|
812
|
+
* service's program imports; its primary declaration must be the file the
|
|
813
|
+
* module resolver lands on, and the resolver must have reached it as an
|
|
814
|
+
* installed dependency. A `paths` alias to the service's own code, or a
|
|
815
|
+
* `declare module` block the service writes, stands in for the package and
|
|
816
|
+
* says nothing about it (`module_local`). The resolver's flag is read, not
|
|
817
|
+
* `program.isSourceFileFromExternalLibrary`: once ts-morph has loaded a
|
|
818
|
+
* dependency, the next program lists it as a root file and the program no
|
|
819
|
+
* longer calls it external. `resolved_file` and `installed_version` both
|
|
820
|
+
* come from that one agreeing answer.
|
|
821
|
+
*/
|
|
822
|
+
readModule(pkg, declaration, runtimeModules = false) {
|
|
823
|
+
const specifier = declaration.moduleSpecifier;
|
|
824
|
+
const resolved = ts.resolveModuleName(pkg, this.probe.fileName, this.program.getCompilerOptions(), this.host, undefined, undefined, this.program.getModeForUsageLocation(this.probe, specifier)).resolvedModule;
|
|
825
|
+
const entry = { package: pkg };
|
|
826
|
+
const done = (reason) => reason ? { entry: { ...entry, reason }, reason } : { entry };
|
|
827
|
+
const moduleSymbol = this.checker.getSymbolAtLocation(specifier);
|
|
828
|
+
const declarations = moduleSymbol?.declarations ?? [];
|
|
829
|
+
if (runtimeModules && RUNTIME_MODULE.test(pkg))
|
|
830
|
+
return this.readRuntimeModule(declarations, entry, done);
|
|
831
|
+
// The module itself, not a service-side augmentation of it.
|
|
832
|
+
const primary = declarations.find(ts.isSourceFile) ?? declarations[0];
|
|
833
|
+
const file = primary?.getSourceFile();
|
|
834
|
+
if (file) {
|
|
835
|
+
entry.resolved_file = file.fileName;
|
|
836
|
+
const agrees = resolved !== undefined && this.isSameInstalledFile(resolved.resolvedFileName, file);
|
|
837
|
+
// Under `node_modules` is not enough: a `paths` alias can point the name
|
|
838
|
+
// at another installed package. The resolver must land in an installed
|
|
839
|
+
// package directory, and that package must be the one named: by its
|
|
840
|
+
// `packageId` (or its separate `@types` package), or, for an `npm:`
|
|
841
|
+
// alias, by the directory name, which the installer takes from the
|
|
842
|
+
// alias. (The package.json comparison beside it always holds:
|
|
843
|
+
// TypeScript builds `packageId` from that same package.json.)
|
|
844
|
+
const installed = agrees && resolved !== undefined ? this.installedPackage(resolved.resolvedFileName) : undefined;
|
|
845
|
+
const packageId = resolved?.packageId;
|
|
846
|
+
const named = installed !== undefined &&
|
|
847
|
+
packageId !== undefined &&
|
|
848
|
+
(isNamedPackage(pkg, packageId.name) ||
|
|
849
|
+
(installed.directoryName === packageNameOf(pkg) && installed.packageName === packageId.name));
|
|
850
|
+
if (named && packageId.version)
|
|
851
|
+
entry.installed_version = packageId.version;
|
|
852
|
+
if (!TYPESCRIPT_FILE.test(file.fileName))
|
|
853
|
+
return done('module_js_only');
|
|
854
|
+
if (!named)
|
|
855
|
+
return done('module_local');
|
|
856
|
+
return done();
|
|
857
|
+
}
|
|
858
|
+
if (!resolved)
|
|
859
|
+
return done('module_unresolved');
|
|
860
|
+
entry.resolved_file = resolved.resolvedFileName;
|
|
861
|
+
const version = resolved.packageId?.version;
|
|
862
|
+
if (version)
|
|
863
|
+
entry.installed_version = version;
|
|
864
|
+
if (!TYPESCRIPT_FILE.test(resolved.resolvedFileName))
|
|
865
|
+
return done('module_js_only');
|
|
866
|
+
// A declaration file with no module symbol exports nothing: every export
|
|
867
|
+
// of it is missing.
|
|
868
|
+
return done();
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* `node:<module>`: the runtime's types package declares it in an ambient
|
|
872
|
+
* `declare module` block, which no module resolution lands on (and which
|
|
873
|
+
* the checker prefers over one that does). It is the runtime's when that
|
|
874
|
+
* package, installed under the service, declares it; a block only the
|
|
875
|
+
* service writes is `module_local`.
|
|
876
|
+
*/
|
|
877
|
+
readRuntimeModule(declarations, entry, done) {
|
|
878
|
+
if (declarations.length === 0)
|
|
879
|
+
return done('module_unresolved');
|
|
880
|
+
const ambient = declarations.find(declaration => {
|
|
881
|
+
const owner = this.packageOf(declaration.getSourceFile());
|
|
882
|
+
return owner !== undefined && RUNTIME_TYPE_PACKAGES.has(owner);
|
|
883
|
+
});
|
|
884
|
+
if (!ambient)
|
|
885
|
+
return done('module_local');
|
|
886
|
+
const file = ambient.getSourceFile();
|
|
887
|
+
entry.resolved_file = file.fileName;
|
|
888
|
+
const version = this.installedPackage(file.fileName)?.version;
|
|
889
|
+
if (version)
|
|
890
|
+
entry.installed_version = version;
|
|
891
|
+
return done();
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* The resolver's file is the checker's file. With two installs of the same
|
|
895
|
+
* name and version, TypeScript loads one and makes the other a redirect to
|
|
896
|
+
* it, so the resolver's copy may be a redirect whose target is the checker's
|
|
897
|
+
* file; that counts only when the target is itself an installed file.
|
|
898
|
+
*/
|
|
899
|
+
isSameInstalledFile(resolvedFileName, file) {
|
|
900
|
+
const resolved = this.program.getSourceFile(resolvedFileName);
|
|
901
|
+
if (resolved === file)
|
|
902
|
+
return true;
|
|
903
|
+
// `redirectInfo` is internal to the compiler, but it is the only record of
|
|
904
|
+
// which copy TypeScript deduplicated a package into.
|
|
905
|
+
const target = resolved
|
|
906
|
+
?.redirectInfo?.redirectTarget;
|
|
907
|
+
return target === file && this.isInstalledFile(file.fileName);
|
|
908
|
+
}
|
|
909
|
+
/** `T`: the type the probe's import gets, and the declaration it names. */
|
|
910
|
+
readExport(declaration) {
|
|
911
|
+
const clause = declaration.importClause;
|
|
912
|
+
const bindings = clause?.namedBindings;
|
|
913
|
+
const local = clause?.name ??
|
|
914
|
+
(bindings && ts.isNamedImports(bindings) ? bindings.elements[0]?.name : undefined);
|
|
915
|
+
const alias = local ? this.checker.getSymbolAtLocation(local) : undefined;
|
|
916
|
+
if (!local || !alias)
|
|
917
|
+
return { reason: 'export_missing' };
|
|
918
|
+
const target = this.checker.getAliasedSymbol(alias);
|
|
919
|
+
// A name the module does not export resolves to the checker's unknown
|
|
920
|
+
// symbol; a type-only export has no value to call.
|
|
921
|
+
if (this.checker.isUnknownSymbol(target) || !(target.flags & ts.SymbolFlags.Value)) {
|
|
922
|
+
return { reason: 'export_missing' };
|
|
923
|
+
}
|
|
924
|
+
const type = this.checker.getTypeOfSymbolAtLocation(alias, local);
|
|
925
|
+
if (this.isOpenTop(type))
|
|
926
|
+
return { reason: 'export_untyped' };
|
|
927
|
+
return { value: { type, target } };
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* HTTP `R`: the export itself, or what its factory `factory` returns. The
|
|
931
|
+
* instance comes from the first overload whose first parameter is an object
|
|
932
|
+
* type (or the only overload). When a `factory` claim in the request for the
|
|
933
|
+
* same factory holds, the overload that satisfies it must be that same one,
|
|
934
|
+
* or the instance is unresolved: a base URL set through one overload says
|
|
935
|
+
* nothing about the instance another overload builds. A factory claim that
|
|
936
|
+
* holds on no overload leaves the rule as it is; the scanner never reads an
|
|
937
|
+
* instance through it.
|
|
938
|
+
*/
|
|
939
|
+
readReceiver(exported, factory, baseUrlKeys) {
|
|
940
|
+
if (factory === undefined)
|
|
941
|
+
return { value: exported };
|
|
942
|
+
const callable = this.callableProperty(exported, factory);
|
|
943
|
+
if ('failure' in callable)
|
|
944
|
+
return { reason: 'factory_unresolved' };
|
|
945
|
+
const signatures = callable.signatures;
|
|
946
|
+
const signature = signatures.find(sig => {
|
|
947
|
+
const first = this.parameterAt(sig, 0);
|
|
948
|
+
return first !== undefined && this.isObjectType(first);
|
|
949
|
+
}) ?? (signatures.length === 1 ? signatures[0] : undefined);
|
|
950
|
+
if (!signature)
|
|
951
|
+
return { reason: 'factory_unresolved' };
|
|
952
|
+
for (const baseUrlKey of baseUrlKeys ?? []) {
|
|
953
|
+
const keyed = signatures.find(sig => this.factorySignatureOutcome(sig, baseUrlKey).rank === Infinity);
|
|
954
|
+
if (keyed !== undefined && keyed !== signature)
|
|
955
|
+
return { reason: 'factory_unresolved' };
|
|
956
|
+
}
|
|
957
|
+
const instance = this.checker.getReturnTypeOfSignature(signature);
|
|
958
|
+
if (this.returnSaysNothing(instance))
|
|
959
|
+
return { reason: 'factory_unresolved' };
|
|
960
|
+
return { value: instance };
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* What a maker or scope signature returns. A generic one is read at its
|
|
964
|
+
* declared type-parameter defaults (contract amendment 2, B6) when `built`,
|
|
965
|
+
* the probe's build of that receiver with no argument and no type argument,
|
|
966
|
+
* resolved to it: TypeScript instantiates such a call at each parameter's
|
|
967
|
+
* default, else at its constraint when `unknown` does not satisfy it, else
|
|
968
|
+
* at `unknown`, which says nothing. That is what a service that passes no
|
|
969
|
+
* type argument gets; a type argument of its own only narrows which names
|
|
970
|
+
* the slots allow. Otherwise the type parameters stay open.
|
|
971
|
+
*/
|
|
972
|
+
returnOf(signature, built) {
|
|
973
|
+
if (built !== undefined && this.instantiates(built, signature))
|
|
974
|
+
return this.checker.getReturnTypeOfSignature(built);
|
|
975
|
+
return this.checker.getReturnTypeOfSignature(signature);
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* `resolved` instantiates an overload that returns the very type
|
|
979
|
+
* `signature` returns: `signature` itself, or another constructor of the
|
|
980
|
+
* same generic class, which builds the same instance. Its type arguments
|
|
981
|
+
* are then `signature`'s too. Another overload's instance says nothing
|
|
982
|
+
* about this one's.
|
|
983
|
+
*/
|
|
984
|
+
instantiates(resolved, signature) {
|
|
985
|
+
// `target` is internal to the compiler, but it is the only record of
|
|
986
|
+
// which overload a resolved call instantiated. Without it, nothing is
|
|
987
|
+
// read at its defaults.
|
|
988
|
+
const target = resolved.target;
|
|
989
|
+
return (target !== undefined &&
|
|
990
|
+
this.checker.getReturnTypeOfSignature(target) === this.checker.getReturnTypeOfSignature(signature));
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* The packages whose declarations are a receiver's own: the named package
|
|
994
|
+
* (and its `@types` package), and every package that declares one of
|
|
995
|
+
* `symbols` (the export at the end of its re-export chain, the receiver's
|
|
996
|
+
* type, its alias, its parts). A meta-package that re-exports a scoped core
|
|
997
|
+
* package's client owns that client's members; a class that extends another
|
|
998
|
+
* package's base does not own what it only inherits. The runtime's type
|
|
999
|
+
* packages and the default library never join, unless the named package is
|
|
1000
|
+
* one of them. `base`, when given, is the home this one extends (a
|
|
1001
|
+
* sub-object's, along a claim's `path`).
|
|
1002
|
+
*/
|
|
1003
|
+
homeOf(pkg, symbols, base) {
|
|
1004
|
+
const named = packageNameOf(pkg);
|
|
1005
|
+
const home = new Set(base ?? [named, typesPackageOf(named)]);
|
|
1006
|
+
const runtime = RUNTIME_MODULE.test(pkg) || RUNTIME_TYPE_PACKAGES.has(named) || RUNTIME_TYPE_PACKAGES.has(typesPackageOf(named));
|
|
1007
|
+
for (const symbol of symbols) {
|
|
1008
|
+
for (const declaration of symbol?.declarations ?? []) {
|
|
1009
|
+
const owner = this.packageOf(declaration.getSourceFile());
|
|
1010
|
+
if (owner === undefined)
|
|
1011
|
+
continue;
|
|
1012
|
+
if (!runtime && RUNTIME_TYPE_PACKAGES.has(owner))
|
|
1013
|
+
continue;
|
|
1014
|
+
home.add(owner);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
return home;
|
|
1018
|
+
}
|
|
1019
|
+
/** The installed package a file belongs to, by its package.json name; never the default library. */
|
|
1020
|
+
packageOf(file) {
|
|
1021
|
+
if (this.program.isSourceFileDefaultLibrary(file))
|
|
1022
|
+
return undefined;
|
|
1023
|
+
const installed = this.installedPackage(file.fileName);
|
|
1024
|
+
return installed === undefined ? undefined : installed.packageName ?? installed.directoryName;
|
|
1025
|
+
}
|
|
1026
|
+
isOwnFile(file, home) {
|
|
1027
|
+
const owner = this.packageOf(file);
|
|
1028
|
+
return owner !== undefined && home.has(owner);
|
|
1029
|
+
}
|
|
1030
|
+
// --------------------------------------------------------------------------
|
|
1031
|
+
// HTTP claims (#1564, unchanged)
|
|
1032
|
+
// --------------------------------------------------------------------------
|
|
1033
|
+
/**
|
|
1034
|
+
* An HTTP claim in the shared shape, judged by the #1564 check its shape
|
|
1035
|
+
* came from (`httpCheck` is the inverse). A shape no #1564 kind produces is
|
|
1036
|
+
* `claim_invalid`.
|
|
1037
|
+
*/
|
|
1038
|
+
judgeHttp(receiver, claim) {
|
|
1039
|
+
if (claim.kind === 'make') {
|
|
1040
|
+
const base = claim.base;
|
|
1041
|
+
if (claim.form !== 'call' ||
|
|
1042
|
+
claim.member === null ||
|
|
1043
|
+
claim.name !== undefined ||
|
|
1044
|
+
claim.handler !== undefined ||
|
|
1045
|
+
claim.prefix !== undefined ||
|
|
1046
|
+
base === undefined ||
|
|
1047
|
+
base.arg !== 0 ||
|
|
1048
|
+
base.key === undefined) {
|
|
1049
|
+
return unchecked('claim_invalid');
|
|
1050
|
+
}
|
|
1051
|
+
return this.judgeFactory(receiver, claim.member, base.key);
|
|
1052
|
+
}
|
|
1053
|
+
if (claim.kind !== 'op' || claim.op !== 'request' || claim.handler || claim.ack || claim.path !== undefined) {
|
|
1054
|
+
return unchecked('claim_invalid');
|
|
1055
|
+
}
|
|
1056
|
+
const name = claim.name;
|
|
1057
|
+
if (name !== undefined && 'bound' in name)
|
|
1058
|
+
return unchecked('claim_invalid');
|
|
1059
|
+
if (claim.method_key) {
|
|
1060
|
+
const at = claim.method_key.arg;
|
|
1061
|
+
const methodKey = claim.method_key.key;
|
|
1062
|
+
if (methodKey === undefined || claim.method !== undefined || claim.options !== undefined) {
|
|
1063
|
+
return unchecked('claim_invalid');
|
|
1064
|
+
}
|
|
1065
|
+
const payload = claim.payload;
|
|
1066
|
+
if (at === 0) {
|
|
1067
|
+
if ((name && (name.arg !== 0 || name.key === undefined)) || (payload && (payload.arg !== 0 || payload.key === undefined))) {
|
|
1068
|
+
return unchecked('claim_invalid');
|
|
1069
|
+
}
|
|
1070
|
+
const selected = this.selectRequest(receiver, claim.member, 'config', name?.key, methodKey, payload?.key);
|
|
1071
|
+
return 'failure' in selected ? selected.failure : VERIFIED;
|
|
1072
|
+
}
|
|
1073
|
+
if (at === 1) {
|
|
1074
|
+
if (!name || name.arg !== 0 || name.key !== undefined || (payload && (payload.arg !== 1 || payload.key === undefined))) {
|
|
1075
|
+
return unchecked('claim_invalid');
|
|
1076
|
+
}
|
|
1077
|
+
const selected = this.selectRequest(receiver, claim.member, 'path_options', undefined, methodKey, payload?.key);
|
|
1078
|
+
return 'failure' in selected ? selected.failure : VERIFIED;
|
|
1079
|
+
}
|
|
1080
|
+
return unchecked('claim_invalid');
|
|
1081
|
+
}
|
|
1082
|
+
if (claim.member === null || !name || name.arg !== 0 || name.key !== undefined)
|
|
1083
|
+
return unchecked('claim_invalid');
|
|
1084
|
+
const hasBody = claim.payload !== undefined || claim.options !== undefined;
|
|
1085
|
+
if (claim.method === undefined && !hasBody)
|
|
1086
|
+
return unchecked('claim_invalid');
|
|
1087
|
+
if (claim.method !== undefined) {
|
|
1088
|
+
const verb = this.judgeVerb(receiver, claim.member, claim.method);
|
|
1089
|
+
if (verb.verdict !== 'verified' || !hasBody)
|
|
1090
|
+
return verb;
|
|
1091
|
+
}
|
|
1092
|
+
if (claim.options !== undefined) {
|
|
1093
|
+
const payload = claim.payload;
|
|
1094
|
+
if (claim.options.arg !== 1 || claim.options.key !== undefined || (payload && (payload.arg !== 1 || payload.key === undefined))) {
|
|
1095
|
+
return unchecked('claim_invalid');
|
|
1096
|
+
}
|
|
1097
|
+
return this.judgeVerbBody(receiver, claim.member, 'path_options', payload?.key);
|
|
1098
|
+
}
|
|
1099
|
+
if (claim.payload.arg !== 1 || claim.payload.key !== undefined)
|
|
1100
|
+
return unchecked('claim_invalid');
|
|
1101
|
+
return this.judgeVerbBody(receiver, claim.member, 'path_body', undefined);
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* `member` is a declared callable property of the receiver; some signature's
|
|
1105
|
+
* first parameter declares `base_url_key` accepting string, and returns a
|
|
1106
|
+
* type that says something.
|
|
1107
|
+
*/
|
|
1108
|
+
judgeFactory(receiver, member, baseUrlKey) {
|
|
1109
|
+
const callable = this.callableProperty(receiver, member);
|
|
1110
|
+
if ('failure' in callable)
|
|
1111
|
+
return callable.failure;
|
|
1112
|
+
let best;
|
|
1113
|
+
for (const signature of callable.signatures) {
|
|
1114
|
+
const result = this.factorySignatureOutcome(signature, baseUrlKey);
|
|
1115
|
+
if (result.rank === Infinity)
|
|
1116
|
+
return VERIFIED;
|
|
1117
|
+
best = furthest(best, result.rank, result.outcome);
|
|
1118
|
+
}
|
|
1119
|
+
return best.outcome;
|
|
1120
|
+
}
|
|
1121
|
+
/** One factory overload against the factory predicate; rank `Infinity` holds. */
|
|
1122
|
+
factorySignatureOutcome(signature, baseUrlKey) {
|
|
1123
|
+
const first = this.keyParameterAt(signature, 0);
|
|
1124
|
+
if (first === undefined)
|
|
1125
|
+
return { rank: 1, outcome: failed('param_missing') };
|
|
1126
|
+
const key = this.keyProperty(first, baseUrlKey, type => this.acceptsString(type));
|
|
1127
|
+
if (!key)
|
|
1128
|
+
return { rank: 2, outcome: this.slotFailure(first, 'key_missing') };
|
|
1129
|
+
const keyType = this.checker.getTypeOfSymbol(key);
|
|
1130
|
+
if (!this.acceptsString(keyType))
|
|
1131
|
+
return { rank: 3, outcome: this.slotFailure(keyType, 'key_not_string') };
|
|
1132
|
+
// The option is there, but what the factory builds cannot be read, so
|
|
1133
|
+
// nothing it returns can be checked either.
|
|
1134
|
+
if (this.returnSaysNothing(this.checker.getReturnTypeOfSignature(signature))) {
|
|
1135
|
+
return { rank: 4, outcome: unchecked('factory_unresolved') };
|
|
1136
|
+
}
|
|
1137
|
+
return { rank: Infinity, outcome: VERIFIED };
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* `method` is `member` upper-cased (ASCII only) and an HTTP method (a method
|
|
1141
|
+
* is not a type-level fact, so this is read off the claim itself); `member`
|
|
1142
|
+
* is a declared callable property whose first parameter accepts string.
|
|
1143
|
+
*/
|
|
1144
|
+
judgeVerb(receiver, member, method) {
|
|
1145
|
+
if (method !== asciiUpperCase(member) || !HTTP_METHODS.has(method)) {
|
|
1146
|
+
return failed('method_not_member_verb');
|
|
1147
|
+
}
|
|
1148
|
+
const callable = this.callableProperty(receiver, member);
|
|
1149
|
+
if ('failure' in callable)
|
|
1150
|
+
return callable.failure;
|
|
1151
|
+
let best;
|
|
1152
|
+
for (const signature of callable.signatures) {
|
|
1153
|
+
const first = this.keyParameterAt(signature, 0);
|
|
1154
|
+
if (first !== undefined && this.acceptsString(first))
|
|
1155
|
+
return VERIFIED;
|
|
1156
|
+
best = furthest(best, 1, first === undefined ? failed('path_not_string') : this.slotFailure(first, 'path_not_string'));
|
|
1157
|
+
}
|
|
1158
|
+
return best.outcome;
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Some signature whose first parameter accepts string has a second one:
|
|
1162
|
+
* open for `path_body`; for `path_options`, an object type with at least one
|
|
1163
|
+
* declared property, `body_key` among them when given.
|
|
1164
|
+
*/
|
|
1165
|
+
judgeVerbBody(receiver, member, args, bodyKey) {
|
|
1166
|
+
const callable = this.callableProperty(receiver, member);
|
|
1167
|
+
if ('failure' in callable)
|
|
1168
|
+
return callable.failure;
|
|
1169
|
+
let best;
|
|
1170
|
+
for (const signature of callable.signatures) {
|
|
1171
|
+
const first = this.keyParameterAt(signature, 0);
|
|
1172
|
+
// An open body is read as declared: a type parameter is the open body
|
|
1173
|
+
// itself, not its constraint.
|
|
1174
|
+
const second = args === 'path_body' ? this.parameterAt(signature, 1) : this.keyParameterAt(signature, 1);
|
|
1175
|
+
if (first === undefined || second === undefined) {
|
|
1176
|
+
best = furthest(best, 1, failed('param_missing'));
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
if (!this.acceptsString(first)) {
|
|
1180
|
+
best = furthest(best, 1, this.slotFailure(first, 'param_missing'));
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
if (args === 'path_body') {
|
|
1184
|
+
if (this.isOpenBody(second))
|
|
1185
|
+
return VERIFIED;
|
|
1186
|
+
best = furthest(best, 2, this.slotFailure(second, 'body_not_open'));
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
if (!this.isObjectType(second) || this.declaredProperties(second).length === 0) {
|
|
1190
|
+
best = furthest(best, 2, this.slotFailure(second, 'options_not_object'));
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
if (bodyKey !== undefined && !this.declaredProperty(second, bodyKey)) {
|
|
1194
|
+
best = furthest(best, 3, failed('key_missing'));
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
return VERIFIED;
|
|
1198
|
+
}
|
|
1199
|
+
return best.outcome;
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* The callee's signatures a request claim can be read through. The callee
|
|
1203
|
+
* is `receiver[member]`, or the receiver itself when `member` is null.
|
|
1204
|
+
*
|
|
1205
|
+
* `config`: the first parameter is an object type declaring `url_key`
|
|
1206
|
+
* accepting string and `method_key` accepting string or an HTTP method
|
|
1207
|
+
* literal. `path_options`: the first parameter accepts string and the second
|
|
1208
|
+
* is an object type declaring `method_key` accepting the same. A
|
|
1209
|
+
* `request_body` claim also needs `body_key` declared there.
|
|
1210
|
+
*
|
|
1211
|
+
* All the keys come from ONE config object: for a union, from one member
|
|
1212
|
+
* that is an object type and not a function type. Keys split across union
|
|
1213
|
+
* members (`{ url } | { method }`) describe no call anyone can make.
|
|
1214
|
+
*/
|
|
1215
|
+
selectRequest(receiver, member, args, urlKey, methodKey, bodyKey) {
|
|
1216
|
+
const callable = member === null ? this.callSignatures(receiver) : this.callableProperty(receiver, member);
|
|
1217
|
+
if ('failure' in callable)
|
|
1218
|
+
return callable;
|
|
1219
|
+
const selected = [];
|
|
1220
|
+
let best;
|
|
1221
|
+
for (const signature of callable.signatures) {
|
|
1222
|
+
const first = this.keyParameterAt(signature, 0);
|
|
1223
|
+
let config = first;
|
|
1224
|
+
if (args === 'path_options') {
|
|
1225
|
+
if (first === undefined || !this.acceptsString(first)) {
|
|
1226
|
+
best = furthest(best, 1, first === undefined ? failed('param_missing') : this.slotFailure(first, 'param_missing'));
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
config = this.keyParameterAt(signature, 1);
|
|
1230
|
+
}
|
|
1231
|
+
if (config === undefined) {
|
|
1232
|
+
best = furthest(best, 1, failed('param_missing'));
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
if (config === VARIADIC || !this.isObjectType(config)) {
|
|
1236
|
+
best = furthest(best, 1, this.slotFailure(config, 'param_missing'));
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
const parts = this.parts(config);
|
|
1240
|
+
const views = parts.length > 1 ? parts.filter(part => this.isPlainObject(part)) : [config];
|
|
1241
|
+
if (views.length === 0)
|
|
1242
|
+
best = furthest(best, 2, failed('key_missing'));
|
|
1243
|
+
for (const view of views) {
|
|
1244
|
+
const outcome = this.requestKeysOutcome(view, args, urlKey, methodKey, bodyKey);
|
|
1245
|
+
if (outcome.rank === Infinity) {
|
|
1246
|
+
selected.push(signature);
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
best = furthest(best, outcome.rank, outcome.outcome);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (selected.length > 0)
|
|
1253
|
+
return { signatures: selected };
|
|
1254
|
+
return { failure: best.outcome };
|
|
1255
|
+
}
|
|
1256
|
+
/** One config object against a request claim's keys; rank `Infinity` holds. */
|
|
1257
|
+
requestKeysOutcome(config, args, urlKey, methodKey, bodyKey) {
|
|
1258
|
+
// A `config` claim names its url key; with none there is nothing to find.
|
|
1259
|
+
const url = args === 'config'
|
|
1260
|
+
? urlKey === undefined
|
|
1261
|
+
? undefined
|
|
1262
|
+
: this.declaredProperty(config, urlKey)
|
|
1263
|
+
: null;
|
|
1264
|
+
const method = this.declaredProperty(config, methodKey);
|
|
1265
|
+
if (url === undefined || !method)
|
|
1266
|
+
return { rank: 2, outcome: failed('key_missing') };
|
|
1267
|
+
const urlType = url === null ? undefined : this.checker.getTypeOfSymbol(url);
|
|
1268
|
+
if (urlType !== undefined && !this.acceptsString(urlType)) {
|
|
1269
|
+
return { rank: 3, outcome: this.slotFailure(urlType, 'key_not_string') };
|
|
1270
|
+
}
|
|
1271
|
+
const methodType = this.checker.getTypeOfSymbol(method);
|
|
1272
|
+
if (!this.acceptsMethod(methodType)) {
|
|
1273
|
+
return { rank: 3, outcome: this.slotFailure(methodType, 'key_not_string') };
|
|
1274
|
+
}
|
|
1275
|
+
if (bodyKey !== undefined && !this.declaredProperty(config, bodyKey)) {
|
|
1276
|
+
return { rank: 4, outcome: failed('key_missing') };
|
|
1277
|
+
}
|
|
1278
|
+
return { rank: Infinity, outcome: VERIFIED };
|
|
1279
|
+
}
|
|
1280
|
+
// --------------------------------------------------------------------------
|
|
1281
|
+
// Message claims (broker, in-process bus, socket)
|
|
1282
|
+
// --------------------------------------------------------------------------
|
|
1283
|
+
/**
|
|
1284
|
+
* A message claim on its receiver; a maker or scope claim also returns the
|
|
1285
|
+
* overloads it holds on. `built` is the probe's no-argument build of the
|
|
1286
|
+
* receiver a maker or scope claim makes (see `returnOf`).
|
|
1287
|
+
*/
|
|
1288
|
+
judgeMessage(receiver, claim, built) {
|
|
1289
|
+
switch (claim.kind) {
|
|
1290
|
+
case 'make':
|
|
1291
|
+
return this.judgeMake(receiver, claim, built);
|
|
1292
|
+
case 'scope':
|
|
1293
|
+
return this.judgeScope(receiver, claim, built);
|
|
1294
|
+
case 'op':
|
|
1295
|
+
return { outcome: this.judgeOp(receiver, claim) };
|
|
1296
|
+
case 'reserved':
|
|
1297
|
+
return { outcome: this.judgeReserved(receiver, claim) };
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* `member` (or the export itself, when null) is callable (`call`) or
|
|
1302
|
+
* constructible (`new`) through signatures the receiver's home packages
|
|
1303
|
+
* declare; some overload takes the base, prefix, name and handler where the
|
|
1304
|
+
* claim's slots put them (contract amendment 2, B2), under the name-slot
|
|
1305
|
+
* rules, and returns a type that says something, read at its type-parameter
|
|
1306
|
+
* defaults (`returnOf`). Those overloads are the ones an instance is read
|
|
1307
|
+
* through. A definition (`task({ id, run })`) is a maker with a `name` and a
|
|
1308
|
+
* `handler` slot; a queue (`new Queue("emails")`) one with a positional name.
|
|
1309
|
+
*/
|
|
1310
|
+
judgeMake(receiver, claim, built) {
|
|
1311
|
+
const layout = this.layout({ base: claim.base, prefix: claim.prefix, name: claim.name, handler: claim.handler });
|
|
1312
|
+
if ('failure' in layout)
|
|
1313
|
+
return { outcome: layout.failure };
|
|
1314
|
+
const callee = this.ownCallee(receiver, [], claim.member, claim.form);
|
|
1315
|
+
if ('failure' in callee)
|
|
1316
|
+
return { outcome: callee.failure };
|
|
1317
|
+
const labels = claim.key_labels ?? {};
|
|
1318
|
+
const holding = [];
|
|
1319
|
+
let best;
|
|
1320
|
+
for (const signature of callee.signatures) {
|
|
1321
|
+
let result = this.messageSignatureOutcome(signature, layout.value, labels);
|
|
1322
|
+
if (result.rank === Infinity && this.returnSaysNothing(this.returnOf(signature, built))) {
|
|
1323
|
+
result = { rank: 6, outcome: unchecked('maker_unresolved') };
|
|
1324
|
+
}
|
|
1325
|
+
if (result.rank === Infinity)
|
|
1326
|
+
holding.push(signature);
|
|
1327
|
+
else
|
|
1328
|
+
best = furthest(best, result.rank, result.outcome);
|
|
1329
|
+
}
|
|
1330
|
+
if (holding.length > 0)
|
|
1331
|
+
return { outcome: VERIFIED, holding };
|
|
1332
|
+
return { outcome: best.outcome };
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* `member` (after `path`) is a home-declared callable member whose name
|
|
1336
|
+
* slot accepts a string, under the name-slot rules, and returns a type that
|
|
1337
|
+
* says something, read at its type-parameter defaults (`returnOf`).
|
|
1338
|
+
*/
|
|
1339
|
+
judgeScope(receiver, claim, built) {
|
|
1340
|
+
const layout = this.layout({ name: claim.name });
|
|
1341
|
+
if ('failure' in layout)
|
|
1342
|
+
return { outcome: layout.failure };
|
|
1343
|
+
const callee = this.ownCallee(receiver, claim.path ?? [], claim.member, 'call');
|
|
1344
|
+
if ('failure' in callee)
|
|
1345
|
+
return { outcome: callee.failure };
|
|
1346
|
+
const labels = claim.key_labels ?? {};
|
|
1347
|
+
const holding = [];
|
|
1348
|
+
let best;
|
|
1349
|
+
for (const signature of callee.signatures) {
|
|
1350
|
+
let result = this.messageSignatureOutcome(signature, layout.value, labels);
|
|
1351
|
+
if (result.rank === Infinity && this.returnSaysNothing(this.returnOf(signature, built))) {
|
|
1352
|
+
result = { rank: 6, outcome: unchecked('scope_unresolved') };
|
|
1353
|
+
}
|
|
1354
|
+
if (result.rank === Infinity)
|
|
1355
|
+
holding.push(signature);
|
|
1356
|
+
else
|
|
1357
|
+
best = furthest(best, result.rank, result.outcome);
|
|
1358
|
+
}
|
|
1359
|
+
if (holding.length > 0)
|
|
1360
|
+
return { outcome: VERIFIED, holding };
|
|
1361
|
+
return { outcome: best.outcome };
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* A send, receive or request op: the home-declared callable member (after
|
|
1365
|
+
* `path`; or the receiver itself, when null) with some overload that takes
|
|
1366
|
+
* the name, payload, handler and acknowledgement where the claim puts them.
|
|
1367
|
+
* A name bound by the maker or the scope must have been bound: the
|
|
1368
|
+
* instance's maker claim has a `name` slot, or the receiver came through a
|
|
1369
|
+
* scope.
|
|
1370
|
+
*/
|
|
1371
|
+
judgeOp(receiver, claim) {
|
|
1372
|
+
if (claim.method !== undefined || claim.method_key !== undefined || claim.options !== undefined) {
|
|
1373
|
+
return unchecked('claim_invalid');
|
|
1374
|
+
}
|
|
1375
|
+
const name = claim.name;
|
|
1376
|
+
// A send carries a payload: a name alone on `send(data)` is the data.
|
|
1377
|
+
if (name === undefined || (claim.op === 'send' && claim.payload === undefined))
|
|
1378
|
+
return unchecked('claim_invalid');
|
|
1379
|
+
let nameSlot;
|
|
1380
|
+
if ('bound' in name) {
|
|
1381
|
+
if (name.bound === 'maker' ? !receiver.makerBindsName : !receiver.scoped)
|
|
1382
|
+
return failed('name_unbound');
|
|
1383
|
+
}
|
|
1384
|
+
else {
|
|
1385
|
+
nameSlot = name;
|
|
1386
|
+
}
|
|
1387
|
+
const layout = this.layout({ name: nameSlot, payload: claim.payload, handler: claim.handler, ack: claim.ack });
|
|
1388
|
+
if ('failure' in layout)
|
|
1389
|
+
return layout.failure;
|
|
1390
|
+
const callee = this.ownCallee(receiver, claim.path ?? [], claim.member, 'call');
|
|
1391
|
+
if ('failure' in callee)
|
|
1392
|
+
return callee.failure;
|
|
1393
|
+
const labels = claim.key_labels ?? {};
|
|
1394
|
+
let best;
|
|
1395
|
+
for (const signature of callee.signatures) {
|
|
1396
|
+
const result = this.messageSignatureOutcome(signature, layout.value, labels);
|
|
1397
|
+
if (result.rank === Infinity)
|
|
1398
|
+
return VERIFIED;
|
|
1399
|
+
best = furthest(best, result.rank, result.outcome);
|
|
1400
|
+
}
|
|
1401
|
+
return best.outcome;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* The declarations spell `name` in one of `member`'s parameters: an
|
|
1405
|
+
* overload whose parameter there is that literal, or a key of the map a
|
|
1406
|
+
* `keyof` constraint reads. The claim carries no slot, so every parameter
|
|
1407
|
+
* is read. A name the library emits itself only ever removes rows, so this
|
|
1408
|
+
* verdict is for the record.
|
|
1409
|
+
*/
|
|
1410
|
+
judgeReserved(receiver, claim) {
|
|
1411
|
+
const callee = this.ownCallee(receiver, claim.path ?? [], claim.member, 'call');
|
|
1412
|
+
if ('failure' in callee)
|
|
1413
|
+
return callee.failure;
|
|
1414
|
+
for (const signature of callee.signatures) {
|
|
1415
|
+
const count = signature.getParameters().length;
|
|
1416
|
+
for (let arg = 0; arg < count; arg++) {
|
|
1417
|
+
if (this.literalsAt(signature, { arg }).has(claim.name))
|
|
1418
|
+
return VERIFIED;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return failed('reserved_not_declared');
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* Where a claim's parts sit. Two parts at one position (`send(data)` read
|
|
1425
|
+
* as both the name and the payload), or a part at a position another part
|
|
1426
|
+
* reads keys of, describe no call: `slots_overlap`.
|
|
1427
|
+
*/
|
|
1428
|
+
layout(parts) {
|
|
1429
|
+
const positional = new Map();
|
|
1430
|
+
const keyed = new Map();
|
|
1431
|
+
for (const [part, slot] of Object.entries(parts)) {
|
|
1432
|
+
if (slot === undefined)
|
|
1433
|
+
continue;
|
|
1434
|
+
if (slot.key === undefined) {
|
|
1435
|
+
if (positional.has(slot.arg) || keyed.has(slot.arg))
|
|
1436
|
+
return { failure: failed('slots_overlap') };
|
|
1437
|
+
positional.set(slot.arg, part);
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (positional.has(slot.arg))
|
|
1441
|
+
return { failure: failed('slots_overlap') };
|
|
1442
|
+
const keys = keyed.get(slot.arg) ?? new Map();
|
|
1443
|
+
if (keys.has(slot.key))
|
|
1444
|
+
return { failure: failed('slots_overlap') };
|
|
1445
|
+
keys.set(slot.key, part);
|
|
1446
|
+
keyed.set(slot.arg, keys);
|
|
1447
|
+
}
|
|
1448
|
+
return { value: { positional, keyed } };
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* One overload against a message claim's layout; rank `Infinity` holds.
|
|
1452
|
+
*
|
|
1453
|
+
* 1. A positional name, base or prefix accepts a string.
|
|
1454
|
+
* 2. A positional name is not a key of an index-signature map; a payload
|
|
1455
|
+
* is there (its type is not read) and is not a callback.
|
|
1456
|
+
* 3. A positional handler or acknowledgement is a function type with a
|
|
1457
|
+
* declared signature. Keyed parts: the object at that argument declares
|
|
1458
|
+
* every claimed key in one view (one union member), a name, base or
|
|
1459
|
+
* prefix key accepting a string and a handler key a declared function.
|
|
1460
|
+
* 4. Every string slot of the call besides the name is accounted for
|
|
1461
|
+
* (design D2, `nameSiblings`).
|
|
1462
|
+
*/
|
|
1463
|
+
messageSignatureOutcome(signature, layout, labels) {
|
|
1464
|
+
for (const [arg, part] of layout.positional) {
|
|
1465
|
+
if (part !== 'name' && part !== 'base' && part !== 'prefix')
|
|
1466
|
+
continue;
|
|
1467
|
+
const declared = this.keyParameterAt(signature, arg);
|
|
1468
|
+
if (declared === undefined)
|
|
1469
|
+
return { rank: 1, outcome: failed('param_missing') };
|
|
1470
|
+
const slot = this.throughConditional(declared);
|
|
1471
|
+
if (!this.acceptsString(slot)) {
|
|
1472
|
+
return { rank: 1, outcome: this.slotFailure(slot, part === 'name' ? 'name_not_string' : 'slot_not_string') };
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
for (const [arg, part] of layout.positional) {
|
|
1476
|
+
if (part === 'name' && this.isIndexKeySlot(signature, arg)) {
|
|
1477
|
+
return { rank: 2, outcome: failed('name_index_key') };
|
|
1478
|
+
}
|
|
1479
|
+
if (part === 'payload') {
|
|
1480
|
+
const payload = this.parameterAt(signature, arg);
|
|
1481
|
+
if (payload === undefined)
|
|
1482
|
+
return { rank: 2, outcome: failed('payload_missing') };
|
|
1483
|
+
// Function versus data is shape: a callback in that place is not the payload.
|
|
1484
|
+
if (this.isFunctionSlot(payload))
|
|
1485
|
+
return { rank: 2, outcome: failed('payload_is_function') };
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
for (const [arg, part] of layout.positional) {
|
|
1489
|
+
if (part !== 'handler' && part !== 'ack')
|
|
1490
|
+
continue;
|
|
1491
|
+
const failure = this.handlerFailure(this.keyParameterAt(signature, arg));
|
|
1492
|
+
if (failure)
|
|
1493
|
+
return { rank: 3, outcome: failure };
|
|
1494
|
+
}
|
|
1495
|
+
const views = new Map();
|
|
1496
|
+
for (const [arg, keys] of layout.keyed) {
|
|
1497
|
+
const slot = this.keyParameterAt(signature, arg);
|
|
1498
|
+
if (slot === undefined)
|
|
1499
|
+
return { rank: 1, outcome: failed('param_missing') };
|
|
1500
|
+
const view = this.viewFor(slot, keys);
|
|
1501
|
+
if ('rank' in view)
|
|
1502
|
+
return view;
|
|
1503
|
+
views.set(arg, view.view);
|
|
1504
|
+
}
|
|
1505
|
+
const ambiguity = this.nameSiblings(signature, layout, views, labels);
|
|
1506
|
+
if (ambiguity)
|
|
1507
|
+
return { rank: 4, outcome: ambiguity };
|
|
1508
|
+
return { rank: Infinity, outcome: VERIFIED };
|
|
1509
|
+
}
|
|
1510
|
+
/**
|
|
1511
|
+
* The object at a keyed argument, read as one view: the whole type, or one
|
|
1512
|
+
* union member that is an object type and not a function type, declaring
|
|
1513
|
+
* every claimed key with the type its part needs.
|
|
1514
|
+
*/
|
|
1515
|
+
viewFor(slot, keys) {
|
|
1516
|
+
if (slot === VARIADIC || !this.isObjectType(slot)) {
|
|
1517
|
+
return { rank: 1, outcome: this.slotFailure(slot, 'param_missing') };
|
|
1518
|
+
}
|
|
1519
|
+
const views = this.keyViews(slot);
|
|
1520
|
+
let best;
|
|
1521
|
+
if (views.length === 0)
|
|
1522
|
+
best = { rank: 2, outcome: failed('key_missing') };
|
|
1523
|
+
for (const view of views) {
|
|
1524
|
+
let failure;
|
|
1525
|
+
for (const [key, part] of keys) {
|
|
1526
|
+
const property = this.declaredProperty(view, key);
|
|
1527
|
+
if (!property) {
|
|
1528
|
+
failure = { rank: 2, outcome: failed('key_missing') };
|
|
1529
|
+
break;
|
|
1530
|
+
}
|
|
1531
|
+
const type = this.checker.getTypeOfSymbol(property);
|
|
1532
|
+
if ((part === 'name' || part === 'base' || part === 'prefix') && !this.acceptsString(this.throughConditional(type))) {
|
|
1533
|
+
failure = { rank: 3, outcome: this.slotFailure(this.throughConditional(type), 'key_not_string') };
|
|
1534
|
+
break;
|
|
1535
|
+
}
|
|
1536
|
+
if (part === 'payload' && this.isFunctionSlot(type)) {
|
|
1537
|
+
failure = { rank: 3, outcome: failed('payload_is_function') };
|
|
1538
|
+
break;
|
|
1539
|
+
}
|
|
1540
|
+
if (part === 'handler' || part === 'ack') {
|
|
1541
|
+
const bad = this.handlerFailure(type);
|
|
1542
|
+
if (bad) {
|
|
1543
|
+
failure = { rank: 3, outcome: bad };
|
|
1544
|
+
break;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
if (!failure)
|
|
1549
|
+
return { view };
|
|
1550
|
+
best = furthest(best, failure.rank, failure.outcome);
|
|
1551
|
+
}
|
|
1552
|
+
return best;
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* The objects a claim's keys at one argument are read from, one at a time:
|
|
1556
|
+
* the whole type, or each union member that is an object type and not a
|
|
1557
|
+
* function type.
|
|
1558
|
+
*/
|
|
1559
|
+
keyViews(slot) {
|
|
1560
|
+
const parts = this.parts(slot);
|
|
1561
|
+
return parts.length > 1 ? parts.filter(part => this.isPlainObject(part)) : [slot];
|
|
1562
|
+
}
|
|
1563
|
+
/**
|
|
1564
|
+
* Strict D2: `name_ambiguous` unless every string slot of the call besides
|
|
1565
|
+
* the name is accounted for, because which of two string slots is the name
|
|
1566
|
+
* is behaviour, not shape. A slot is accounted for when the claim assigns it
|
|
1567
|
+
* a part (payload, handler, base, prefix), or, for a key of an object at
|
|
1568
|
+
* the call, when `key_labels` labels it `not_name`. A positional string the
|
|
1569
|
+
* claim leaves unassigned cannot be labelled, so it always competes. A
|
|
1570
|
+
* slot or key counts as a string exactly when a name there would
|
|
1571
|
+
* (`siblingTakesString`): a conditional counts when one of its branches
|
|
1572
|
+
* takes a string (fail closed, carrick#1687).
|
|
1573
|
+
*
|
|
1574
|
+
* The labels must agree with the claim: at most one key is labelled `name`,
|
|
1575
|
+
* and only the claim's own name key; that key is never labelled `not_name`.
|
|
1576
|
+
* A `not_name` label for a key an overload does not declare is ignored
|
|
1577
|
+
* (one label map serves every overload). No name, no rule.
|
|
1578
|
+
*
|
|
1579
|
+
* Rest parameters: a rest the claim puts a payload or handler in belongs
|
|
1580
|
+
* wholly to that part; one that holds the name holds other names too; from
|
|
1581
|
+
* the first variable element of a tuple rest (`[...channels: string[], cb]`)
|
|
1582
|
+
* no position is fixed, so a name there is one of many. A rest the claim
|
|
1583
|
+
* leaves unassigned competes when its element accepts a string, or when it
|
|
1584
|
+
* cannot be read at all (`...args: Rest<B>`, a conditional on a type
|
|
1585
|
+
* parameter), since it could hold a string slot.
|
|
1586
|
+
*/
|
|
1587
|
+
nameSiblings(signature, layout, views, labels) {
|
|
1588
|
+
const namedAt = [...layout.positional].find(([, part]) => part === 'name')?.[0];
|
|
1589
|
+
let nameKey;
|
|
1590
|
+
for (const keys of layout.keyed.values()) {
|
|
1591
|
+
for (const [key, part] of keys)
|
|
1592
|
+
if (part === 'name')
|
|
1593
|
+
nameKey = key;
|
|
1594
|
+
}
|
|
1595
|
+
if (namedAt === undefined && nameKey === undefined)
|
|
1596
|
+
return undefined;
|
|
1597
|
+
const ambiguous = failed('name_ambiguous');
|
|
1598
|
+
const nameLabels = Object.keys(labels).filter(key => labels[key] === 'name');
|
|
1599
|
+
if (nameLabels.length > 1)
|
|
1600
|
+
return ambiguous;
|
|
1601
|
+
if (nameLabels.length === 1 && nameLabels[0] !== nameKey)
|
|
1602
|
+
return ambiguous;
|
|
1603
|
+
if (nameKey !== undefined && labels[nameKey] === 'not_name')
|
|
1604
|
+
return ambiguous;
|
|
1605
|
+
// A string key at the call the claim neither assigns nor labels.
|
|
1606
|
+
const unaccountedKey = (keys, assigned) => keys.some(key => !assigned.has(key) && labels[key] !== 'not_name');
|
|
1607
|
+
const none = new Map();
|
|
1608
|
+
const takesString = (slot) => slot !== undefined && this.siblingTakesString(slot);
|
|
1609
|
+
// An argument the claim gives no part: a string, an object with a string
|
|
1610
|
+
// key nobody accounts for, or a rest the verifier cannot read, which could
|
|
1611
|
+
// hold either and no label can account for (fail closed, carrick#1687).
|
|
1612
|
+
const competes = (slot) => slot === VARIADIC || takesString(slot) || unaccountedKey(this.stringKeys(slot), none);
|
|
1613
|
+
// Every object the claim reads keys of: its other string keys.
|
|
1614
|
+
for (const [arg, keys] of layout.keyed) {
|
|
1615
|
+
const view = views.get(arg);
|
|
1616
|
+
if (view && unaccountedKey(this.stringKeys(view), keys))
|
|
1617
|
+
return ambiguous;
|
|
1618
|
+
}
|
|
1619
|
+
const parameters = signature.getParameters();
|
|
1620
|
+
const last = parameters[parameters.length - 1];
|
|
1621
|
+
const lastDeclaration = last?.valueDeclaration;
|
|
1622
|
+
const restIndex = lastDeclaration && ts.isParameter(lastDeclaration) && lastDeclaration.dotDotDotToken ? parameters.length - 1 : -1;
|
|
1623
|
+
const fixed = restIndex === -1 ? parameters.length : restIndex;
|
|
1624
|
+
const assigned = (i) => layout.positional.has(i) || layout.keyed.has(i);
|
|
1625
|
+
for (let i = 0; i < fixed; i++) {
|
|
1626
|
+
if (assigned(i))
|
|
1627
|
+
continue;
|
|
1628
|
+
if (competes(this.keyParameterAt(signature, i)))
|
|
1629
|
+
return ambiguous;
|
|
1630
|
+
}
|
|
1631
|
+
if (restIndex === -1)
|
|
1632
|
+
return undefined;
|
|
1633
|
+
const rest = this.checker.getTypeOfSymbol(last);
|
|
1634
|
+
if (this.checker.isTupleType(rest)) {
|
|
1635
|
+
const elements = this.checker.getTypeArguments(rest);
|
|
1636
|
+
const flags = rest.target.elementFlags;
|
|
1637
|
+
const variable = flags.findIndex(flag => (flag & ts.ElementFlags.Variable) !== 0);
|
|
1638
|
+
const fixedLength = variable === -1 ? elements.length : variable;
|
|
1639
|
+
for (let i = restIndex; i < restIndex + fixedLength; i++) {
|
|
1640
|
+
if (assigned(i))
|
|
1641
|
+
continue;
|
|
1642
|
+
if (competes(this.keyParameterAt(signature, i)))
|
|
1643
|
+
return ambiguous;
|
|
1644
|
+
}
|
|
1645
|
+
if (variable !== -1) {
|
|
1646
|
+
const unfixed = [...layout.positional].filter(([arg]) => arg >= restIndex + variable).map(([, part]) => part);
|
|
1647
|
+
if (unfixed.includes('name'))
|
|
1648
|
+
return ambiguous;
|
|
1649
|
+
const element = elements[variable];
|
|
1650
|
+
const each = element !== undefined && this.checker.isArrayType(element)
|
|
1651
|
+
? this.checker.getTypeArguments(element)[0]
|
|
1652
|
+
: element;
|
|
1653
|
+
if (unfixed.length === 0 && competes(each === undefined ? undefined : this.throughConstraint(each))) {
|
|
1654
|
+
return ambiguous;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
return undefined;
|
|
1658
|
+
}
|
|
1659
|
+
const inRest = [...layout.positional].filter(([arg]) => arg >= restIndex).map(([, part]) => part);
|
|
1660
|
+
if (inRest.includes('name'))
|
|
1661
|
+
return takesString(this.keyParameterAt(signature, restIndex)) ? ambiguous : undefined;
|
|
1662
|
+
// A rest the claim puts a payload or handler in is that part's.
|
|
1663
|
+
if (inRest.length > 0)
|
|
1664
|
+
return undefined;
|
|
1665
|
+
return competes(this.keyParameterAt(signature, restIndex)) ? ambiguous : undefined;
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* A slot beside the name takes a string, read as a name there is read: a
|
|
1669
|
+
* conditional through its branches (`throughConditional`). One whose
|
|
1670
|
+
* branch says nothing counts no more than `any` does.
|
|
1671
|
+
*/
|
|
1672
|
+
siblingTakesString(slot) {
|
|
1673
|
+
return this.acceptsString(this.throughConditional(slot));
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* The string-accepting keys of an argument: every key some object part of
|
|
1677
|
+
* it declares, read through a type parameter's constraint.
|
|
1678
|
+
*/
|
|
1679
|
+
stringKeys(slot) {
|
|
1680
|
+
if (slot === undefined || slot === VARIADIC)
|
|
1681
|
+
return [];
|
|
1682
|
+
const keys = new Set();
|
|
1683
|
+
for (const part of this.parts(this.throughConstraint(slot))) {
|
|
1684
|
+
if (!this.isObjectLike(part))
|
|
1685
|
+
continue;
|
|
1686
|
+
for (const property of this.namedProperties(part)) {
|
|
1687
|
+
if (this.siblingTakesString(this.checker.getTypeOfSymbol(property)))
|
|
1688
|
+
keys.add(property.getName());
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return [...keys];
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* The parameter at `index` is declared as a key of a map with an index
|
|
1695
|
+
* signature: `keyof M`, or a type parameter constrained by one, where `M`
|
|
1696
|
+
* (or its constraint) declares `[key: string]: ...`. Such a slot takes any
|
|
1697
|
+
* key the map could hold, so it says nothing about a name. Read at the
|
|
1698
|
+
* declaration, where the map is still the type parameter the library wrote.
|
|
1699
|
+
*/
|
|
1700
|
+
isIndexKeySlot(signature, index) {
|
|
1701
|
+
const declaration = signature.getDeclaration();
|
|
1702
|
+
if (!declaration)
|
|
1703
|
+
return false;
|
|
1704
|
+
const parameters = declaration.parameters;
|
|
1705
|
+
const parameter = parameters[Math.min(index, parameters.length - 1)];
|
|
1706
|
+
if (!parameter?.type)
|
|
1707
|
+
return false;
|
|
1708
|
+
if (index >= parameters.length && !parameter.dotDotDotToken)
|
|
1709
|
+
return false;
|
|
1710
|
+
return this.keysOfIndexMap(this.checker.getTypeFromTypeNode(parameter.type), parameter.type, 0);
|
|
1711
|
+
}
|
|
1712
|
+
keysOfIndexMap(type, node, depth) {
|
|
1713
|
+
if (depth > 6)
|
|
1714
|
+
return false;
|
|
1715
|
+
// `keyof M` written against a concrete map is resolved at once to the
|
|
1716
|
+
// map's keys (`string | number` for an index signature); the node keeps
|
|
1717
|
+
// what was written.
|
|
1718
|
+
if (node && ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.KeyOfKeyword) {
|
|
1719
|
+
return this.isIndexMap(this.checker.getTypeFromTypeNode(node.type));
|
|
1720
|
+
}
|
|
1721
|
+
if (type.flags & ts.TypeFlags.Index) {
|
|
1722
|
+
return this.isIndexMap(type.type);
|
|
1723
|
+
}
|
|
1724
|
+
if (type.flags & ts.TypeFlags.TypeParameter) {
|
|
1725
|
+
const constraintNode = this.declaredConstraint(type);
|
|
1726
|
+
return (constraintNode !== undefined &&
|
|
1727
|
+
this.keysOfIndexMap(this.checker.getTypeFromTypeNode(constraintNode), constraintNode, depth + 1));
|
|
1728
|
+
}
|
|
1729
|
+
if (type.isUnionOrIntersection()) {
|
|
1730
|
+
return type.types.some(part => this.keysOfIndexMap(part, undefined, depth + 1));
|
|
1731
|
+
}
|
|
1732
|
+
return false;
|
|
1733
|
+
}
|
|
1734
|
+
/** The constraint a type parameter's declaration writes, unresolved (`K extends keyof M`). */
|
|
1735
|
+
declaredConstraint(type) {
|
|
1736
|
+
return (type.getSymbol()?.declarations ?? []).find(ts.isTypeParameterDeclaration)?.constraint;
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* The map a `keyof` reads is a concrete map with an index signature. A map
|
|
1740
|
+
* the library takes as a type parameter (an event map defaulting to an
|
|
1741
|
+
* index signature) is the service's to type: its own type argument only
|
|
1742
|
+
* narrows which names the slot allows and never moves the name to another
|
|
1743
|
+
* argument, so the key slot reads as a string slot (contract amendment 2,
|
|
1744
|
+
* B6, the `index_key_generic_map` reading, now the only one).
|
|
1745
|
+
*/
|
|
1746
|
+
isIndexMap(map) {
|
|
1747
|
+
if (map.flags & ts.TypeFlags.TypeParameter)
|
|
1748
|
+
return false;
|
|
1749
|
+
return this.hasIndexSignature(map);
|
|
1750
|
+
}
|
|
1751
|
+
hasIndexSignature(type) {
|
|
1752
|
+
const target = type.flags & ts.TypeFlags.TypeParameter ? this.checker.getBaseConstraintOfType(type) ?? type : type;
|
|
1753
|
+
return this.checker.getIndexInfosOfType(this.checker.getApparentType(target)).length > 0;
|
|
1754
|
+
}
|
|
1755
|
+
/**
|
|
1756
|
+
* A handler or acknowledgement slot: every part besides null and undefined
|
|
1757
|
+
* is a function type with a declared signature. `Function`, `any`,
|
|
1758
|
+
* `unknown`, an unconstrained type parameter and `(...args: any[])` say
|
|
1759
|
+
* nothing about a handler (`handler_untyped`); a part that is not a
|
|
1760
|
+
* function (an options object, a string) is `handler_not_function`.
|
|
1761
|
+
*/
|
|
1762
|
+
handlerFailure(declared) {
|
|
1763
|
+
if (declared === undefined)
|
|
1764
|
+
return failed('param_missing');
|
|
1765
|
+
// A listener typed by deferred conditional machinery (`ListenerOf<Ev>`)
|
|
1766
|
+
// is read through its branches; one that says nothing is untyped.
|
|
1767
|
+
const slot = this.throughConditional(this.throughConstraint(declared));
|
|
1768
|
+
if (slot === VARIADIC)
|
|
1769
|
+
return unchecked('handler_untyped');
|
|
1770
|
+
const parts = this.parts(slot);
|
|
1771
|
+
if (parts.length === 0)
|
|
1772
|
+
return failed('handler_not_function');
|
|
1773
|
+
for (const part of parts) {
|
|
1774
|
+
if (this.isOpenTop(part) || this.isUnconstrained(part))
|
|
1775
|
+
return unchecked('handler_untyped');
|
|
1776
|
+
const signatures = part.getCallSignatures();
|
|
1777
|
+
if (signatures.length === 0) {
|
|
1778
|
+
return this.isEmptyObject(part) ? unchecked('handler_untyped') : failed('handler_not_function');
|
|
1779
|
+
}
|
|
1780
|
+
if (signatures.every(signature => this.isUntypedSignature(signature)))
|
|
1781
|
+
return unchecked('handler_untyped');
|
|
1782
|
+
}
|
|
1783
|
+
return undefined;
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* A name typed by a conditional (`IdOf<T> = T extends Def<infer Id> ? Id
|
|
1787
|
+
* : never`) is read through what its branches allow. When a branch says
|
|
1788
|
+
* nothing (`... ? Id : any`), so does the slot: the checker's constraint of
|
|
1789
|
+
* such a conditional drops the `any` branch and would read as a string.
|
|
1790
|
+
*/
|
|
1791
|
+
throughConditional(slot) {
|
|
1792
|
+
if (slot === VARIADIC)
|
|
1793
|
+
return slot;
|
|
1794
|
+
const parts = this.parts(slot);
|
|
1795
|
+
if (parts.length !== 1 || !(parts[0].flags & ts.TypeFlags.Conditional))
|
|
1796
|
+
return slot;
|
|
1797
|
+
const node = parts[0].root.node;
|
|
1798
|
+
const branchSaysNothing = [node.trueType, node.falseType].some(branch => {
|
|
1799
|
+
const type = this.checker.getTypeFromTypeNode(branch);
|
|
1800
|
+
return (type.flags & ts.TypeFlags.Never) === 0 && this.returnSaysNothing(type);
|
|
1801
|
+
});
|
|
1802
|
+
if (branchSaysNothing)
|
|
1803
|
+
return VARIADIC;
|
|
1804
|
+
return this.checker.getBaseConstraintOfType(parts[0]) ?? slot;
|
|
1805
|
+
}
|
|
1806
|
+
/** Every part besides null and undefined is callable: a function, not data. */
|
|
1807
|
+
isFunctionSlot(declared) {
|
|
1808
|
+
const slot = this.throughConstraint(declared);
|
|
1809
|
+
if (slot === VARIADIC)
|
|
1810
|
+
return false;
|
|
1811
|
+
const parts = this.parts(slot);
|
|
1812
|
+
return parts.length > 0 && parts.every(part => !this.isOpenTop(part) && part.getCallSignatures().length > 0);
|
|
1813
|
+
}
|
|
1814
|
+
/** `(...args: any[])` or `(...args: unknown[])`: a signature that declares nothing. */
|
|
1815
|
+
isUntypedSignature(signature) {
|
|
1816
|
+
const parameters = signature.getParameters();
|
|
1817
|
+
if (parameters.length !== 1)
|
|
1818
|
+
return false;
|
|
1819
|
+
const declaration = parameters[0].valueDeclaration;
|
|
1820
|
+
if (!declaration || !ts.isParameter(declaration) || !declaration.dotDotDotToken)
|
|
1821
|
+
return false;
|
|
1822
|
+
const rest = this.checker.getTypeOfSymbol(parameters[0]);
|
|
1823
|
+
if (this.isOpenTop(rest))
|
|
1824
|
+
return true;
|
|
1825
|
+
return this.checker.isArrayType(rest) && this.isOpenTop(this.checker.getTypeArguments(rest)[0]);
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* The string literals a slot spells: the literal parts of its type, through
|
|
1829
|
+
* a type parameter's constraint, and the property names of a map a `keyof`
|
|
1830
|
+
* reads, both as instantiated and as written at the declaration.
|
|
1831
|
+
*/
|
|
1832
|
+
literalsAt(signature, at) {
|
|
1833
|
+
const literals = new Set();
|
|
1834
|
+
let slot = this.parameterAt(signature, at.arg);
|
|
1835
|
+
if (slot !== undefined && slot !== VARIADIC && at.key !== undefined) {
|
|
1836
|
+
const property = this.declaredProperty(slot, at.key);
|
|
1837
|
+
slot = property ? this.checker.getTypeOfSymbol(property) : undefined;
|
|
1838
|
+
}
|
|
1839
|
+
if (slot !== undefined && slot !== VARIADIC)
|
|
1840
|
+
this.spell(slot, literals, 0);
|
|
1841
|
+
if (at.key === undefined) {
|
|
1842
|
+
const parameters = signature.getDeclaration()?.parameters;
|
|
1843
|
+
const node = parameters?.[Math.min(at.arg, parameters.length - 1)]?.type;
|
|
1844
|
+
if (node)
|
|
1845
|
+
this.spell(this.checker.getTypeFromTypeNode(node), literals, 0);
|
|
1846
|
+
}
|
|
1847
|
+
return literals;
|
|
1848
|
+
}
|
|
1849
|
+
spell(type, into, depth) {
|
|
1850
|
+
if (depth > 6)
|
|
1851
|
+
return;
|
|
1852
|
+
if (type.isStringLiteral()) {
|
|
1853
|
+
into.add(type.value);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
if (type.isUnionOrIntersection()) {
|
|
1857
|
+
for (const part of type.types)
|
|
1858
|
+
this.spell(part, into, depth + 1);
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (type.flags & ts.TypeFlags.TypeParameter) {
|
|
1862
|
+
const constraintNode = this.declaredConstraint(type);
|
|
1863
|
+
this.spell(constraintNode ? this.checker.getTypeFromTypeNode(constraintNode) : this.checker.getBaseConstraintOfType(type) ?? type, into, depth + 1);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
if (type.flags & ts.TypeFlags.Index) {
|
|
1867
|
+
let map = type.type;
|
|
1868
|
+
if (map.flags & ts.TypeFlags.TypeParameter) {
|
|
1869
|
+
map = this.checker.getDefaultFromTypeParameter(map) ?? this.checker.getBaseConstraintOfType(map) ?? map;
|
|
1870
|
+
}
|
|
1871
|
+
for (const property of this.checker.getPropertiesOfType(this.checker.getApparentType(map))) {
|
|
1872
|
+
if (!isSymbolKeyed(property))
|
|
1873
|
+
into.add(property.getName());
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* The callee of a message claim: the claim's member `path` walked from the
|
|
1879
|
+
* receiver hop by hop (`client.tasks.trigger`), then `member` of the object
|
|
1880
|
+
* reached (or that object itself, when null), called (`call`) or
|
|
1881
|
+
* constructed (`new`), through the signatures its home packages declare.
|
|
1882
|
+
* A member, or every signature, that only another package declares (the
|
|
1883
|
+
* runtime's event emitter a library class extends, a base class from a
|
|
1884
|
+
* dependency) is `member_inherited`. Each hop must be a home member too;
|
|
1885
|
+
* the object it reaches adds the packages that declare its type to the
|
|
1886
|
+
* home, never the runtime's, and a hop whose type says nothing has no
|
|
1887
|
+
* members to read (`member_untyped`).
|
|
1888
|
+
*/
|
|
1889
|
+
ownCallee(receiver, hops, member, form) {
|
|
1890
|
+
let holder = receiver;
|
|
1891
|
+
for (const hop of hops) {
|
|
1892
|
+
const step = this.ownMember(holder, hop);
|
|
1893
|
+
if ('failure' in step)
|
|
1894
|
+
return step;
|
|
1895
|
+
const type = step.type;
|
|
1896
|
+
if (this.saysNothing(type))
|
|
1897
|
+
return { failure: unchecked('member_untyped') };
|
|
1898
|
+
holder = { type, home: this.homeOf(receiver.pkg, typeSymbols(type), holder.home) };
|
|
1899
|
+
}
|
|
1900
|
+
let type = holder.type;
|
|
1901
|
+
// A member of a base the holder binds with its own type (see `isBoundByOwnType`).
|
|
1902
|
+
let boundBase = false;
|
|
1903
|
+
if (member !== null) {
|
|
1904
|
+
const step = this.ownMember(holder, member);
|
|
1905
|
+
if ('failure' in step)
|
|
1906
|
+
return step;
|
|
1907
|
+
type = step.type;
|
|
1908
|
+
boundBase = step.bound;
|
|
1909
|
+
}
|
|
1910
|
+
const all = form === 'new'
|
|
1911
|
+
? this.checker.getNonNullableType(type).getConstructSignatures()
|
|
1912
|
+
: this.checker.getNonNullableType(type).getCallSignatures();
|
|
1913
|
+
const library = all.filter(signature => {
|
|
1914
|
+
const file = this.signatureFile(signature, type, form);
|
|
1915
|
+
return file !== undefined && this.isLibraryFile(file);
|
|
1916
|
+
});
|
|
1917
|
+
const home = holder.home;
|
|
1918
|
+
const own = boundBase
|
|
1919
|
+
? library
|
|
1920
|
+
: library.filter(signature => this.isOwnFile(this.signatureFile(signature, type, form), home));
|
|
1921
|
+
if (own.length > 0)
|
|
1922
|
+
return { signatures: own };
|
|
1923
|
+
if (library.length > 0)
|
|
1924
|
+
return { failure: failed('member_inherited') };
|
|
1925
|
+
return { failure: this.slotFailure(type, form === 'new' ? 'member_not_constructible' : 'member_not_callable') };
|
|
1926
|
+
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Member `name` of `holder`, when its home packages declare it, or when it
|
|
1929
|
+
* sits on another package's base that the holder binds with its own type
|
|
1930
|
+
* (`bound`).
|
|
1931
|
+
*/
|
|
1932
|
+
ownMember(holder, name) {
|
|
1933
|
+
const apparent = this.checker.getApparentType(this.checker.getNonNullableType(holder.type));
|
|
1934
|
+
const property = this.declaredProperty(holder.type, name);
|
|
1935
|
+
if (!property)
|
|
1936
|
+
return { failure: failed('member_missing') };
|
|
1937
|
+
const type = this.checker.getTypeOfSymbol(property);
|
|
1938
|
+
if (this.isOwnMember(property, apparent, holder.home))
|
|
1939
|
+
return { type, bound: false };
|
|
1940
|
+
if (this.isBoundByOwnType(property, holder))
|
|
1941
|
+
return { type, bound: true };
|
|
1942
|
+
return { failure: failed('member_inherited') };
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* An inherited emitter the client binds to its own interface counts as
|
|
1946
|
+
* declared: the member is declared on a base type another package writes,
|
|
1947
|
+
* and the holder's class binds that base with at least one concrete type
|
|
1948
|
+
* argument (not a type parameter) its home packages declare:
|
|
1949
|
+
* `class Socket<L, E> extends Emitter<L, E, SocketReservedEvents>`. The
|
|
1950
|
+
* runtime's emitter, extended with no type of the package's own, is not
|
|
1951
|
+
* bound; nor is a base bound only through the class's type parameters.
|
|
1952
|
+
*/
|
|
1953
|
+
isBoundByOwnType(property, holder) {
|
|
1954
|
+
const owners = (property.declarations ?? [])
|
|
1955
|
+
.map(declaration => declaration.parent)
|
|
1956
|
+
.filter((owner) => owner !== undefined && (ts.isClassLike(owner) || ts.isInterfaceDeclaration(owner)));
|
|
1957
|
+
if (owners.length === 0)
|
|
1958
|
+
return false;
|
|
1959
|
+
const targetOf = (type) => (type.objectFlags ?? 0) & ts.ObjectFlags.Reference ? type.target : type;
|
|
1960
|
+
const isOwnType = (type) => !(type.flags & ts.TypeFlags.TypeParameter) &&
|
|
1961
|
+
typeSymbols(type).some(symbol => (symbol?.declarations ?? []).some(declaration => this.isOwnFile(declaration.getSourceFile(), holder.home)));
|
|
1962
|
+
const visit = (type, depth) => {
|
|
1963
|
+
if (depth > 8)
|
|
1964
|
+
return false;
|
|
1965
|
+
if (type.isIntersection())
|
|
1966
|
+
return type.types.some(part => visit(part, depth + 1));
|
|
1967
|
+
const target = targetOf(type);
|
|
1968
|
+
if (!((target.objectFlags ?? 0) & ts.ObjectFlags.ClassOrInterface))
|
|
1969
|
+
return false;
|
|
1970
|
+
for (const base of this.checker.getBaseTypes(target)) {
|
|
1971
|
+
const declarations = targetOf(base).getSymbol()?.declarations ?? [];
|
|
1972
|
+
if (owners.some(owner => declarations.includes(owner))) {
|
|
1973
|
+
const bound = (base.objectFlags ?? 0) & ts.ObjectFlags.Reference
|
|
1974
|
+
? this.checker.getTypeArguments(base)
|
|
1975
|
+
: [];
|
|
1976
|
+
if (bound.some(isOwnType))
|
|
1977
|
+
return true;
|
|
1978
|
+
continue;
|
|
1979
|
+
}
|
|
1980
|
+
if (visit(base, depth + 1))
|
|
1981
|
+
return true;
|
|
1982
|
+
}
|
|
1983
|
+
return false;
|
|
1984
|
+
};
|
|
1985
|
+
return visit(this.checker.getNonNullableType(holder.type), 0);
|
|
1986
|
+
}
|
|
1987
|
+
/**
|
|
1988
|
+
* The file that wrote a signature. A class's construct signatures are the
|
|
1989
|
+
* class's own, wherever the constructor they reuse was written: a class
|
|
1990
|
+
* with no constructor of its own is built through its base's (or a default
|
|
1991
|
+
* one with no declaration at all), and still builds an instance of itself.
|
|
1992
|
+
*/
|
|
1993
|
+
signatureFile(signature, callee, form) {
|
|
1994
|
+
if (form === 'new') {
|
|
1995
|
+
const declaration = callee.getSymbol()?.declarations?.find(ts.isClassLike);
|
|
1996
|
+
if (declaration)
|
|
1997
|
+
return declaration.getSourceFile();
|
|
1998
|
+
}
|
|
1999
|
+
return signature.getDeclaration()?.getSourceFile();
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Some declaration of the member is in one of the receiver's own packages.
|
|
2003
|
+
* A member a mapped type makes has no declaration of its own; it is own when
|
|
2004
|
+
* it reaches the receiver along types the own packages (or the default
|
|
2005
|
+
* library's utility types, `Record`) declare, and the service's own blocks
|
|
2006
|
+
* do not name it.
|
|
2007
|
+
*/
|
|
2008
|
+
isOwnMember(property, listing, home) {
|
|
2009
|
+
const declarations = property.declarations ?? [];
|
|
2010
|
+
if (declarations.length === 0) {
|
|
2011
|
+
return (!this.isNamedByServiceAugmentation(property.getName()) &&
|
|
2012
|
+
this.isListedByLibraryType(listing, property.getName(), file => this.program.isSourceFileDefaultLibrary(file) || this.isOwnFile(file, home)));
|
|
2013
|
+
}
|
|
2014
|
+
return declarations.some(declaration => this.isOwnFile(declaration.getSourceFile(), home));
|
|
2015
|
+
}
|
|
2016
|
+
// --------------------------------------------------------------------------
|
|
2017
|
+
// Surface listing (carrick#1660)
|
|
2018
|
+
// --------------------------------------------------------------------------
|
|
2019
|
+
/**
|
|
2020
|
+
* One specifier's declared surface (see `LibraryClaimsVerifier.listSurface`).
|
|
2021
|
+
* `built` answers the probe's no-argument build of a receiver an export
|
|
2022
|
+
* makes, resolved, when the probe holds one.
|
|
2023
|
+
*/
|
|
2024
|
+
listPackage(pkg, probe, maxEntries, only, built) {
|
|
2025
|
+
this.setPackage(pkg);
|
|
2026
|
+
// Runtime modules on: a `node:` specifier is listed from the runtime's
|
|
2027
|
+
// types package, as the message roles read it.
|
|
2028
|
+
const moduleRead = this.readModule(pkg, probe.namespace, true);
|
|
2029
|
+
const surface = { package: pkg, truncated: 0, exports: [] };
|
|
2030
|
+
if (moduleRead.entry.resolved_file)
|
|
2031
|
+
surface.resolved_file = moduleRead.entry.resolved_file;
|
|
2032
|
+
if (moduleRead.entry.installed_version)
|
|
2033
|
+
surface.installed_version = moduleRead.entry.installed_version;
|
|
2034
|
+
if (moduleRead.reason)
|
|
2035
|
+
return { ...surface, reason: moduleRead.reason };
|
|
2036
|
+
const exported = this.listedExports(probe, only);
|
|
2037
|
+
if (!exported)
|
|
2038
|
+
return { ...surface, reason: 'module_unresolved' };
|
|
2039
|
+
let budget = maxEntries;
|
|
2040
|
+
let dropped = 0;
|
|
2041
|
+
const take = () => {
|
|
2042
|
+
if (budget > 0) {
|
|
2043
|
+
budget -= 1;
|
|
2044
|
+
return true;
|
|
2045
|
+
}
|
|
2046
|
+
dropped += 1;
|
|
2047
|
+
return false;
|
|
2048
|
+
};
|
|
2049
|
+
// Three passes: every export and receiver, then every member name, then signatures.
|
|
2050
|
+
const names = [];
|
|
2051
|
+
const fills = [];
|
|
2052
|
+
for (const { name, target, type } of exported) {
|
|
2053
|
+
if (!take())
|
|
2054
|
+
continue;
|
|
2055
|
+
const entry = { export: name, receivers: [] };
|
|
2056
|
+
surface.exports.push(entry);
|
|
2057
|
+
if (this.isOpenTop(type))
|
|
2058
|
+
continue;
|
|
2059
|
+
const receiverOf = (receiverName, receiverType) => {
|
|
2060
|
+
if (!take())
|
|
2061
|
+
return;
|
|
2062
|
+
const home = this.homeOf(pkg, [target, ...typeSymbols(receiverType)]);
|
|
2063
|
+
entry.receivers.push(this.outlineReceiver(pkg, receiverName, receiverType, home, take, names, fills));
|
|
2064
|
+
};
|
|
2065
|
+
receiverOf('export', type);
|
|
2066
|
+
for (const maker of this.makers(pkg, target, type)) {
|
|
2067
|
+
const made = this.madeBy(maker.signatures, built(name, maker.receiver));
|
|
2068
|
+
if (!made)
|
|
2069
|
+
continue;
|
|
2070
|
+
// One level below the export, what the member builds must declare a callable member.
|
|
2071
|
+
if (maker.member &&
|
|
2072
|
+
!this.namedProperties(made).some(member => this.librarySignatures(this.checker.getTypeOfSymbol(member), 'call').length > 0)) {
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
2075
|
+
receiverOf(maker.receiver, made);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
for (const run of names)
|
|
2079
|
+
run();
|
|
2080
|
+
for (const fill of fills)
|
|
2081
|
+
fill();
|
|
2082
|
+
surface.truncated = dropped;
|
|
2083
|
+
return surface;
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* The receivers each export of a specifier can make, by receiver id, for
|
|
2087
|
+
* the probe statements that build them (see `LibraryClaimsVerifier.listSurface`).
|
|
2088
|
+
*/
|
|
2089
|
+
makersOf(pkg, probe, only) {
|
|
2090
|
+
this.setPackage(pkg);
|
|
2091
|
+
if (this.readModule(pkg, probe.namespace, true).reason)
|
|
2092
|
+
return [];
|
|
2093
|
+
return (this.listedExports(probe, only) ?? [])
|
|
2094
|
+
.filter(({ type }) => !this.isOpenTop(type))
|
|
2095
|
+
.map(({ name, target, type }) => ({
|
|
2096
|
+
export: name,
|
|
2097
|
+
receivers: this.makers(pkg, target, type).map(maker => maker.receiver),
|
|
2098
|
+
}))
|
|
2099
|
+
.filter(({ receivers }) => receivers.length > 0);
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* The value exports a specifier lists (every one, or only those `only`
|
|
2103
|
+
* names), sorted by name: what its namespace holds, and `default` for a
|
|
2104
|
+
* module that exports a value whole (`export =`), as a default import gets
|
|
2105
|
+
* it, when that import reads a typed value (`readExport`, as the verifier
|
|
2106
|
+
* reads `default`). Such a module's namespace holds its statics, and its
|
|
2107
|
+
* `prototype`, which no service imports. Undefined when the specifier
|
|
2108
|
+
* resolves to no module symbol.
|
|
2109
|
+
*/
|
|
2110
|
+
listedExports(probe, only) {
|
|
2111
|
+
const moduleSymbol = this.checker.getSymbolAtLocation(probe.namespace.moduleSpecifier);
|
|
2112
|
+
if (!moduleSymbol)
|
|
2113
|
+
return undefined;
|
|
2114
|
+
const wanted = (name) => only === undefined || only.includes(name);
|
|
2115
|
+
const all = this.checker.getExportsOfModule(moduleSymbol);
|
|
2116
|
+
const listed = [];
|
|
2117
|
+
for (const symbol of all) {
|
|
2118
|
+
const target = symbol.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(symbol) : symbol;
|
|
2119
|
+
if (this.checker.isUnknownSymbol(target) ||
|
|
2120
|
+
(target.flags & ts.SymbolFlags.Value) === 0 ||
|
|
2121
|
+
(target.flags & ts.SymbolFlags.Prototype) !== 0 ||
|
|
2122
|
+
!wanted(symbol.getName())) {
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
listed.push({ name: symbol.getName(), target, type: this.checker.getTypeOfSymbolAtLocation(symbol, probe.namespace) });
|
|
2126
|
+
}
|
|
2127
|
+
if (moduleSymbol.exports?.has(ts.InternalSymbolName.ExportEquals) &&
|
|
2128
|
+
!all.some(symbol => symbol.getName() === 'default') &&
|
|
2129
|
+
wanted('default')) {
|
|
2130
|
+
const read = this.readExport(probe.defaultImport);
|
|
2131
|
+
if (!('reason' in read))
|
|
2132
|
+
listed.push({ name: 'default', target: read.value.target, type: read.value.type });
|
|
2133
|
+
}
|
|
2134
|
+
return listed.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* The ways an export makes a receiver, in listing order: calling it
|
|
2138
|
+
* (`instance:()`), constructing it (`instance:new`), then calling or
|
|
2139
|
+
* constructing each member (`instance:<member>`, `instance:new:<member>`),
|
|
2140
|
+
* each through the signatures a maker claim reads there (`ownCallee`): its
|
|
2141
|
+
* home's own, or a base's it binds with its own type. A maker the export
|
|
2142
|
+
* only inherits from another package is none the verifier reads
|
|
2143
|
+
* (`member_inherited`).
|
|
2144
|
+
*/
|
|
2145
|
+
makers(pkg, target, type) {
|
|
2146
|
+
const holder = this.surfaceHolder(pkg, type, this.homeOf(pkg, [target, ...typeSymbols(type)]));
|
|
2147
|
+
const makers = [];
|
|
2148
|
+
for (const [form, receiver] of [['call', 'instance:()'], ['new', 'instance:new']]) {
|
|
2149
|
+
const callee = this.ownCallee(holder, [], null, form);
|
|
2150
|
+
if ('signatures' in callee)
|
|
2151
|
+
makers.push({ receiver, member: false, signatures: callee.signatures });
|
|
2152
|
+
}
|
|
2153
|
+
for (const property of sortedByName(this.namedProperties(type))) {
|
|
2154
|
+
for (const [form, prefix] of [['call', 'instance:'], ['new', 'instance:new:']]) {
|
|
2155
|
+
const callee = this.ownCallee(holder, [], property.getName(), form);
|
|
2156
|
+
if ('signatures' in callee) {
|
|
2157
|
+
makers.push({ receiver: `${prefix}${property.getName()}`, member: true, signatures: callee.signatures });
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
return makers;
|
|
2162
|
+
}
|
|
2163
|
+
/** A receiver read for the listing as a message claim reads it (`ownCallee`). */
|
|
2164
|
+
surfaceHolder(pkg, type, home) {
|
|
2165
|
+
return { type, pkg, home, makerBindsName: false, scoped: false };
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* What a maker builds: each overload's return, read at its declared
|
|
2169
|
+
* type-parameter defaults where `built` (the probe's no-argument build of
|
|
2170
|
+
* the receiver) instantiates it (`returnOf`), less the returns that say
|
|
2171
|
+
* nothing, which no maker claim holds on. Listed when what is left is one
|
|
2172
|
+
* object type.
|
|
2173
|
+
*/
|
|
2174
|
+
madeBy(signatures, built) {
|
|
2175
|
+
const returns = [
|
|
2176
|
+
...new Set(signatures.map(sig => this.returnOf(sig, built)).filter(type => !this.returnSaysNothing(type))),
|
|
2177
|
+
];
|
|
2178
|
+
return returns.length === 1 && this.isObjectType(returns[0]) ? returns[0] : undefined;
|
|
2179
|
+
}
|
|
2180
|
+
/** The call (or construct) signatures of `type` an installed package or the default library declares. */
|
|
2181
|
+
librarySignatures(type, form) {
|
|
2182
|
+
const nonNullable = this.checker.getNonNullableType(type);
|
|
2183
|
+
const all = form === 'new' ? nonNullable.getConstructSignatures() : nonNullable.getCallSignatures();
|
|
2184
|
+
return all.filter(signature => {
|
|
2185
|
+
const file = this.signatureFile(signature, nonNullable, form);
|
|
2186
|
+
return file !== undefined && this.isLibraryFile(file);
|
|
2187
|
+
});
|
|
2188
|
+
}
|
|
2189
|
+
/**
|
|
2190
|
+
* A receiver, now; its member names later (`names`), once every export and
|
|
2191
|
+
* receiver of the package is listed; its call, construct and member
|
|
2192
|
+
* signatures last (`fills`), once every name is.
|
|
2193
|
+
*/
|
|
2194
|
+
outlineReceiver(pkg, receiverName, type, home, take, names, fills) {
|
|
2195
|
+
const receiver = { receiver: receiverName, members: [] };
|
|
2196
|
+
const holder = this.surfaceHolder(pkg, type, home);
|
|
2197
|
+
// The receiver's own call and construct signatures, as a claim with a
|
|
2198
|
+
// null member reads them.
|
|
2199
|
+
const call = this.ownCallee(holder, [], null, 'call');
|
|
2200
|
+
if ('signatures' in call)
|
|
2201
|
+
fills.push(() => (receiver.call = this.listSignatures(call.signatures, take)));
|
|
2202
|
+
const construct = this.ownCallee(holder, [], null, 'new');
|
|
2203
|
+
if ('signatures' in construct) {
|
|
2204
|
+
fills.push(() => (receiver.construct = this.listSignatures(construct.signatures, take)));
|
|
2205
|
+
}
|
|
2206
|
+
if (this.isOpenTop(type))
|
|
2207
|
+
return receiver;
|
|
2208
|
+
names.push(() => {
|
|
2209
|
+
for (const property of sortedByName(this.namedProperties(type))) {
|
|
2210
|
+
const library = this.librarySignatures(this.checker.getTypeOfSymbol(property), 'call');
|
|
2211
|
+
if (library.length === 0 || !take())
|
|
2212
|
+
continue;
|
|
2213
|
+
// Own when an op claim reads the member at all (`ownCallee`): its
|
|
2214
|
+
// home declares it, or it sits on a base the receiver binds with its
|
|
2215
|
+
// own type, and its home writes a signature of it. An inherited
|
|
2216
|
+
// member is listed with every library signature.
|
|
2217
|
+
const callee = this.ownCallee(holder, [], property.getName(), 'call');
|
|
2218
|
+
const own = 'signatures' in callee;
|
|
2219
|
+
const member = { name: property.getName(), own, signatures: [] };
|
|
2220
|
+
receiver.members.push(member);
|
|
2221
|
+
fills.push(() => (member.signatures = this.listSignatures(own ? callee.signatures : library, take)));
|
|
2222
|
+
}
|
|
2223
|
+
});
|
|
2224
|
+
return receiver;
|
|
2225
|
+
}
|
|
2226
|
+
listSignatures(signatures, take) {
|
|
2227
|
+
const listed = [];
|
|
2228
|
+
for (const signature of signatures) {
|
|
2229
|
+
if (!take())
|
|
2230
|
+
continue;
|
|
2231
|
+
const params = [];
|
|
2232
|
+
const parameters = signature.getParameters();
|
|
2233
|
+
for (let index = 0; index < parameters.length; index++) {
|
|
2234
|
+
if (!take())
|
|
2235
|
+
continue;
|
|
2236
|
+
const parameter = parameters[index];
|
|
2237
|
+
const declaration = parameter.valueDeclaration;
|
|
2238
|
+
const isParameter = declaration !== undefined && ts.isParameter(declaration);
|
|
2239
|
+
// What a claim part at this argument is checked against: a rest's
|
|
2240
|
+
// element, a type parameter's constraint (`keyParameterAt`).
|
|
2241
|
+
const slot = this.keyParameterAt(signature, index);
|
|
2242
|
+
const param = {
|
|
2243
|
+
name: parameter.getName(),
|
|
2244
|
+
optional: isParameter && (declaration.questionToken !== undefined || declaration.initializer !== undefined),
|
|
2245
|
+
rest: isParameter && declaration.dotDotDotToken !== undefined,
|
|
2246
|
+
type: this.printed(this.checker.getTypeOfSymbol(parameter)),
|
|
2247
|
+
// The checks a positional name and a positional handler pass there.
|
|
2248
|
+
accepts_string: slot !== undefined && this.acceptsString(this.throughConditional(slot)),
|
|
2249
|
+
function: this.handlerFailure(slot) === undefined,
|
|
2250
|
+
};
|
|
2251
|
+
if (slot !== undefined && slot !== VARIADIC && this.isObjectType(slot) && this.handlerFailure(slot) !== undefined) {
|
|
2252
|
+
const keys = this.listKeys(slot, take);
|
|
2253
|
+
if (keys.length > 0)
|
|
2254
|
+
param.keys = keys;
|
|
2255
|
+
}
|
|
2256
|
+
const literals = this.literalsAt(signature, { arg: index });
|
|
2257
|
+
if (literals.size > 0)
|
|
2258
|
+
param.literals = [...literals].sort();
|
|
2259
|
+
params.push(param);
|
|
2260
|
+
}
|
|
2261
|
+
listed.push({ params, returns: this.printed(this.checker.getReturnTypeOfSignature(signature)) });
|
|
2262
|
+
}
|
|
2263
|
+
return listed;
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* The keys a claim can name at an object argument, by name: every key of
|
|
2267
|
+
* each view the verifier reads one at a time (`keyViews`). A
|
|
2268
|
+
* key accepts a string, or is a handler, when it does so in some view, as
|
|
2269
|
+
* a keyed name or handler is checked (`viewFor`). It is optional when a
|
|
2270
|
+
* call can leave it out: some view marks it optional or does not declare it.
|
|
2271
|
+
*/
|
|
2272
|
+
listKeys(slot, take) {
|
|
2273
|
+
const views = this.keyViews(slot);
|
|
2274
|
+
const keys = new Map();
|
|
2275
|
+
for (const view of views) {
|
|
2276
|
+
for (const property of this.namedProperties(view)) {
|
|
2277
|
+
const type = this.checker.getTypeOfSymbol(property);
|
|
2278
|
+
const read = {
|
|
2279
|
+
name: property.getName(),
|
|
2280
|
+
optional: (property.flags & ts.SymbolFlags.Optional) !== 0,
|
|
2281
|
+
accepts_string: this.acceptsString(this.throughConditional(type)),
|
|
2282
|
+
function: this.handlerFailure(type) === undefined,
|
|
2283
|
+
views: 1,
|
|
2284
|
+
};
|
|
2285
|
+
const seen = keys.get(read.name);
|
|
2286
|
+
if (!seen) {
|
|
2287
|
+
keys.set(read.name, read);
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
seen.optional = seen.optional || read.optional;
|
|
2291
|
+
seen.accepts_string = seen.accepts_string || read.accepts_string;
|
|
2292
|
+
seen.function = seen.function || read.function;
|
|
2293
|
+
seen.views += 1;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
return [...keys.values()]
|
|
2297
|
+
.sort((a, b) => byName(a.name, b.name))
|
|
2298
|
+
.filter(() => take())
|
|
2299
|
+
.map(({ views: declaredIn, ...key }) => ({ ...key, optional: key.optional || declaredIn < views.length }));
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* A type as the declarations print it, with the service root written as
|
|
2303
|
+
* `<root>` (the checker spells a type no entry exports through the file that
|
|
2304
|
+
* declares it), cut to a length a listing can carry. The root goes first,
|
|
2305
|
+
* so where the cut falls does not depend on where the package is installed.
|
|
2306
|
+
*/
|
|
2307
|
+
printed(type) {
|
|
2308
|
+
const node = this.checker.typeToTypeNode(type, undefined, PRINT_FLAGS);
|
|
2309
|
+
const text = node === undefined ? this.checker.typeToString(type) : printTypeNode(this.orderUnions(node));
|
|
2310
|
+
return truncate(this.scrubbed(text));
|
|
2311
|
+
}
|
|
2312
|
+
/** `text` with the service root written as `<root>`. */
|
|
2313
|
+
scrubbed(text) {
|
|
2314
|
+
for (const root of this.printedRoots)
|
|
2315
|
+
text = text.split(root).join('<root>');
|
|
2316
|
+
return text;
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* `node` with the members of every union in it, at every depth (inside a
|
|
2320
|
+
* function or a conditional type too), sorted by their own print with the
|
|
2321
|
+
* root scrubbed, compared by UTF-16 code unit. The compiler prints a union
|
|
2322
|
+
* in the order it made the members, which moves with whatever the program
|
|
2323
|
+
* read first.
|
|
2324
|
+
*/
|
|
2325
|
+
orderUnions(node) {
|
|
2326
|
+
const result = ts.transform(node, [
|
|
2327
|
+
context => root => {
|
|
2328
|
+
const visit = (child) => {
|
|
2329
|
+
const next = ts.visitEachChild(child, visit, context);
|
|
2330
|
+
if (!ts.isUnionTypeNode(next))
|
|
2331
|
+
return next;
|
|
2332
|
+
const keyed = next.types.map(member => ({ member, key: this.scrubbed(printTypeNode(member)) }));
|
|
2333
|
+
keyed.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
2334
|
+
return ts.factory.updateUnionTypeNode(next, ts.factory.createNodeArray(keyed.map(({ member }) => member)));
|
|
2335
|
+
};
|
|
2336
|
+
return ts.visitNode(root, visit);
|
|
2337
|
+
},
|
|
2338
|
+
]);
|
|
2339
|
+
const [ordered] = result.transformed;
|
|
2340
|
+
result.dispose();
|
|
2341
|
+
return ordered;
|
|
2342
|
+
}
|
|
2343
|
+
// --------------------------------------------------------------------------
|
|
2344
|
+
// Definitions
|
|
2345
|
+
// --------------------------------------------------------------------------
|
|
2346
|
+
/**
|
|
2347
|
+
* A declared property `name` of `type` whose type has a call signature. A
|
|
2348
|
+
* member typed `any`, `unknown` or `{}` says nothing (`member_untyped`).
|
|
2349
|
+
*/
|
|
2350
|
+
callableProperty(type, name) {
|
|
2351
|
+
const property = this.declaredProperty(type, name);
|
|
2352
|
+
if (!property)
|
|
2353
|
+
return { failure: failed('member_missing') };
|
|
2354
|
+
return this.callSignatures(this.checker.getTypeOfSymbol(property));
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* The call signatures the library declares. A service `declare module`
|
|
2358
|
+
* block can add an overload to a library member; that signature is the
|
|
2359
|
+
* service's claim about the library, not the library's.
|
|
2360
|
+
*/
|
|
2361
|
+
callSignatures(type) {
|
|
2362
|
+
const signatures = this.checker
|
|
2363
|
+
.getNonNullableType(type)
|
|
2364
|
+
.getCallSignatures()
|
|
2365
|
+
.filter(signature => {
|
|
2366
|
+
const declaration = signature.getDeclaration();
|
|
2367
|
+
return declaration !== undefined && this.isLibraryFile(declaration.getSourceFile());
|
|
2368
|
+
});
|
|
2369
|
+
if (signatures.length > 0)
|
|
2370
|
+
return { signatures };
|
|
2371
|
+
return { failure: this.slotFailure(type, 'member_not_callable') };
|
|
2372
|
+
}
|
|
2373
|
+
/**
|
|
2374
|
+
* A declared property of `type`: one the checker lists on its apparent type
|
|
2375
|
+
* with null and undefined removed, and not one every value of that kind
|
|
2376
|
+
* inherits (`constructor`, `toString`, a primitive wrapper's members). An
|
|
2377
|
+
* index signature does not count. Nor does a property typed `never` (or
|
|
2378
|
+
* only `undefined`): it cannot be passed. And a library declares it: at
|
|
2379
|
+
* least one declaration sits in an installed package or the default
|
|
2380
|
+
* library, so a member only the service's own module augmentation adds is
|
|
2381
|
+
* not the package's.
|
|
2382
|
+
*/
|
|
2383
|
+
declaredProperty(slot, name) {
|
|
2384
|
+
return this.namedProperties(slot).find(property => property.getName() === name);
|
|
2385
|
+
}
|
|
2386
|
+
/**
|
|
2387
|
+
* The property `name` of a parameter a key is looked for on. It is the
|
|
2388
|
+
* declared property of the whole type; failing that, for a union, one that an
|
|
2389
|
+
* object constituent declares and whose type passes `accepts`. A constituent
|
|
2390
|
+
* counts only when it is an object type that says something and is not a
|
|
2391
|
+
* function type, so defaults written as `Options | ((parent) => Options)`
|
|
2392
|
+
* are read through `Options`. When a constituent declares the name but no
|
|
2393
|
+
* declaration passes `accepts`, that property is returned for the caller to
|
|
2394
|
+
* reject.
|
|
2395
|
+
*/
|
|
2396
|
+
keyProperty(slot, name, accepts) {
|
|
2397
|
+
const whole = this.declaredProperty(slot, name);
|
|
2398
|
+
if (whole || slot === VARIADIC)
|
|
2399
|
+
return whole;
|
|
2400
|
+
let declaredOnly;
|
|
2401
|
+
for (const part of this.parts(slot)) {
|
|
2402
|
+
if (!this.isPlainObject(part))
|
|
2403
|
+
continue;
|
|
2404
|
+
const property = this.declaredProperty(part, name);
|
|
2405
|
+
if (!property)
|
|
2406
|
+
continue;
|
|
2407
|
+
if (accepts(this.checker.getTypeOfSymbol(property)))
|
|
2408
|
+
return property;
|
|
2409
|
+
declaredOnly ??= property;
|
|
2410
|
+
}
|
|
2411
|
+
return declaredOnly;
|
|
2412
|
+
}
|
|
2413
|
+
/**
|
|
2414
|
+
* An object type that is not a function type. (One that says nothing
|
|
2415
|
+
* declares no property, so it never supplies a key.)
|
|
2416
|
+
*/
|
|
2417
|
+
isPlainObject(type) {
|
|
2418
|
+
return (this.isObjectLike(type) &&
|
|
2419
|
+
type.getCallSignatures().length === 0 &&
|
|
2420
|
+
type.getConstructSignatures().length === 0);
|
|
2421
|
+
}
|
|
2422
|
+
/**
|
|
2423
|
+
* A parameter a key is looked for on, or that must accept a string: read
|
|
2424
|
+
* through a rest parameter's element and through a type parameter's
|
|
2425
|
+
* constraint. A type parameter with no constraint, or one of `any` or
|
|
2426
|
+
* `unknown`, stays as it is and says nothing.
|
|
2427
|
+
*/
|
|
2428
|
+
keyParameterAt(signature, index) {
|
|
2429
|
+
const slot = this.parameterAt(signature, index, true);
|
|
2430
|
+
return slot === undefined ? undefined : this.throughConstraint(slot);
|
|
2431
|
+
}
|
|
2432
|
+
throughConstraint(slot) {
|
|
2433
|
+
if (slot === VARIADIC)
|
|
2434
|
+
return slot;
|
|
2435
|
+
const parts = this.parts(slot);
|
|
2436
|
+
if (parts.length !== 1 || !(parts[0].flags & ts.TypeFlags.TypeParameter))
|
|
2437
|
+
return slot;
|
|
2438
|
+
// An `any` or `unknown` constraint says nothing, as the bare parameter does.
|
|
2439
|
+
return this.checker.getBaseConstraintOfType(parts[0]) ?? slot;
|
|
2440
|
+
}
|
|
2441
|
+
/**
|
|
2442
|
+
* The declared properties a claim can name: every one keyed by a string. A
|
|
2443
|
+
* member or key keyed by a unique symbol (`[Symbol.iterator]`, the runtime
|
|
2444
|
+
* emitter's `[captureRejectionSymbol]`) has no name a claim can carry, and
|
|
2445
|
+
* the checker names it with a symbol id that moves whenever anything
|
|
2446
|
+
* earlier in the process does (`__@iterator@84`): it is never listed, and
|
|
2447
|
+
* it neither satisfies nor blocks a slot or a name (D2).
|
|
2448
|
+
*/
|
|
2449
|
+
namedProperties(slot) {
|
|
2450
|
+
return this.declaredProperties(slot).filter(property => !isSymbolKeyed(property));
|
|
2451
|
+
}
|
|
2452
|
+
declaredProperties(slot) {
|
|
2453
|
+
if (slot === VARIADIC)
|
|
2454
|
+
return [];
|
|
2455
|
+
const cached = this.declared.get(slot);
|
|
2456
|
+
if (cached)
|
|
2457
|
+
return cached;
|
|
2458
|
+
const apparent = this.checker.getApparentType(this.checker.getNonNullableType(slot));
|
|
2459
|
+
const declared = this.checker
|
|
2460
|
+
.getPropertiesOfType(apparent)
|
|
2461
|
+
.filter(property => !this.isBuiltinMember(property) &&
|
|
2462
|
+
this.isLibraryDeclared(property, apparent) &&
|
|
2463
|
+
!this.isAbsent(property));
|
|
2464
|
+
this.declared.set(slot, declared);
|
|
2465
|
+
return declared;
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Some declaration of the property is in an installed package or the
|
|
2469
|
+
* default library. A member a mapped type makes (`extends Record<'get',
|
|
2470
|
+
* Fn>`) has no declaration of its own; it counts when the type listing it
|
|
2471
|
+
* was written by the library.
|
|
2472
|
+
*/
|
|
2473
|
+
isLibraryDeclared(property, listing) {
|
|
2474
|
+
const declarations = property.declarations ?? [];
|
|
2475
|
+
if (declarations.length === 0) {
|
|
2476
|
+
return (!this.isNamedByServiceAugmentation(property.getName()) &&
|
|
2477
|
+
this.isListedByLibraryType(listing, property.getName(), file => this.isLibraryFile(file)));
|
|
2478
|
+
}
|
|
2479
|
+
return declarations.some(declaration => this.isLibraryFile(declaration.getSourceFile()));
|
|
2480
|
+
}
|
|
2481
|
+
/** Judge the next check as a claim about `pkg`. */
|
|
2482
|
+
setPackage(pkg) {
|
|
2483
|
+
const named = packageNameOf(pkg);
|
|
2484
|
+
if (named !== this.currentPackage)
|
|
2485
|
+
this.declared.clear();
|
|
2486
|
+
this.currentPackage = named;
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* The service's own `declare module '<this package>'` (or one of its
|
|
2490
|
+
* subpaths) or `declare global` blocks declare a member of this name,
|
|
2491
|
+
* compared without case. A mapped type's member has no declaration of its
|
|
2492
|
+
* own, so when the service adds a key to the interface a library mapped
|
|
2493
|
+
* type iterates (`Record<keyof MethodMap, Fn>`, with or without `& string`,
|
|
2494
|
+
* or re-cased by an `as Lowercase<...>` remap), nothing on the member says
|
|
2495
|
+
* the service put it there. Blocks for other packages do not count: a
|
|
2496
|
+
* service augments many packages, and their member names say nothing about
|
|
2497
|
+
* this one.
|
|
2498
|
+
*/
|
|
2499
|
+
isNamedByServiceAugmentation(name) {
|
|
2500
|
+
const names = this.serviceAugmentedNames();
|
|
2501
|
+
const lower = name.toLowerCase();
|
|
2502
|
+
return Boolean(names.get(this.currentPackage)?.has(lower) || names.get('global')?.has(lower));
|
|
2503
|
+
}
|
|
2504
|
+
serviceAugmentedNames() {
|
|
2505
|
+
if (this.augmentedNames)
|
|
2506
|
+
return this.augmentedNames;
|
|
2507
|
+
const byPackage = new Map();
|
|
2508
|
+
const collect = (node, names) => {
|
|
2509
|
+
if ((ts.isPropertySignature(node) ||
|
|
2510
|
+
ts.isMethodSignature(node) ||
|
|
2511
|
+
ts.isPropertyDeclaration(node) ||
|
|
2512
|
+
ts.isMethodDeclaration(node) ||
|
|
2513
|
+
ts.isEnumMember(node)) &&
|
|
2514
|
+
(ts.isIdentifier(node.name) || ts.isStringLiteral(node.name) || ts.isNumericLiteral(node.name))) {
|
|
2515
|
+
names.add(node.name.text.toLowerCase());
|
|
2516
|
+
}
|
|
2517
|
+
ts.forEachChild(node, child => collect(child, names));
|
|
2518
|
+
};
|
|
2519
|
+
for (const file of this.program.getSourceFiles()) {
|
|
2520
|
+
if (file === this.probe || this.isLibraryFile(file))
|
|
2521
|
+
continue;
|
|
2522
|
+
for (const statement of file.statements) {
|
|
2523
|
+
if (!ts.isModuleDeclaration(statement) || !statement.body)
|
|
2524
|
+
continue;
|
|
2525
|
+
const key = ts.isStringLiteral(statement.name)
|
|
2526
|
+
? packageNameOf(statement.name.text)
|
|
2527
|
+
: statement.flags & ts.NodeFlags.GlobalAugmentation
|
|
2528
|
+
? 'global'
|
|
2529
|
+
: undefined;
|
|
2530
|
+
if (key === undefined)
|
|
2531
|
+
continue;
|
|
2532
|
+
let names = byPackage.get(key);
|
|
2533
|
+
if (!names)
|
|
2534
|
+
byPackage.set(key, (names = new Set()));
|
|
2535
|
+
collect(statement.body, names);
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
this.augmentedNames = byPackage;
|
|
2539
|
+
return byPackage;
|
|
2540
|
+
}
|
|
2541
|
+
/**
|
|
2542
|
+
* Member `name`, which has no declaration of its own, reaches `listing`
|
|
2543
|
+
* along a path of types `isAllowed` files alone declare. The path runs
|
|
2544
|
+
* through the types that list the member: an intersection's parts
|
|
2545
|
+
* (`type Client = {...} & Record<Alias, Fn> & Fn`), and an interface's base
|
|
2546
|
+
* types (`interface S extends Base`), each declared only in allowed files.
|
|
2547
|
+
* The library's `Record` is; a base interface the service extended with its
|
|
2548
|
+
* own mapped type is not, at any depth.
|
|
2549
|
+
*/
|
|
2550
|
+
isListedByLibraryType(listing, name, isAllowed) {
|
|
2551
|
+
const listedBy = (types) => types.some(part => {
|
|
2552
|
+
const apparent = this.checker.getApparentType(part);
|
|
2553
|
+
return (this.checker.getPropertyOfType(apparent, name) !== undefined &&
|
|
2554
|
+
this.isListedByLibraryType(apparent, name, isAllowed));
|
|
2555
|
+
});
|
|
2556
|
+
if (listing.isIntersection())
|
|
2557
|
+
return listedBy(listing.types);
|
|
2558
|
+
if (!this.isDeclaredOnlyIn(listing.getSymbol(), isAllowed))
|
|
2559
|
+
return false;
|
|
2560
|
+
const objectFlags = listing.objectFlags ?? 0;
|
|
2561
|
+
const target = objectFlags & ts.ObjectFlags.Reference ? listing.target : listing;
|
|
2562
|
+
const targetFlags = target.objectFlags ?? 0;
|
|
2563
|
+
// A type literal or mapped type lists its members itself.
|
|
2564
|
+
if (!(targetFlags & ts.ObjectFlags.ClassOrInterface))
|
|
2565
|
+
return true;
|
|
2566
|
+
return listedBy(this.checker.getBaseTypes(target));
|
|
2567
|
+
}
|
|
2568
|
+
isDeclaredOnlyIn(symbol, isAllowed) {
|
|
2569
|
+
const declarations = symbol?.declarations ?? [];
|
|
2570
|
+
return declarations.length > 0 && declarations.every(declaration => isAllowed(declaration.getSourceFile()));
|
|
2571
|
+
}
|
|
2572
|
+
/** An installed package's file, or the default library's. */
|
|
2573
|
+
isLibraryFile(file) {
|
|
2574
|
+
return this.program.isSourceFileDefaultLibrary(file) || this.isInstalledFile(file.fileName);
|
|
2575
|
+
}
|
|
2576
|
+
isInstalledFile(fileName) {
|
|
2577
|
+
return this.installedPackage(fileName) !== undefined;
|
|
2578
|
+
}
|
|
2579
|
+
/**
|
|
2580
|
+
* The installed package a file belongs to. Its path, taken relative to the
|
|
2581
|
+
* service root, has a package directory after its last `node_modules`
|
|
2582
|
+
* segment (two segments for a scope), holding a package.json. A service
|
|
2583
|
+
* source file in a directory that happens to be named `node_modules`, or a
|
|
2584
|
+
* repository checked out under a `node_modules` ancestor, is not installed.
|
|
2585
|
+
* An install hoisted above the service root (`../../node_modules/pkg`) and
|
|
2586
|
+
* a pnpm store (`node_modules/.pnpm/pkg@1/node_modules/pkg`) are.
|
|
2587
|
+
*/
|
|
2588
|
+
installedPackage(fileName) {
|
|
2589
|
+
const segments = path.relative(this.root, this.realpath(fileName)).split(path.sep);
|
|
2590
|
+
const last = segments.lastIndexOf('node_modules');
|
|
2591
|
+
if (last < 0)
|
|
2592
|
+
return undefined;
|
|
2593
|
+
const width = segments[last + 1]?.startsWith('@') ? 2 : 1;
|
|
2594
|
+
const nameSegments = segments.slice(last + 1, last + 1 + width);
|
|
2595
|
+
// No package.json there (a file directly under node_modules included) means no package.
|
|
2596
|
+
const directory = path.resolve(this.root, ...segments.slice(0, last + 1 + width));
|
|
2597
|
+
let known = this.packageDirectories.get(directory);
|
|
2598
|
+
if (known === undefined) {
|
|
2599
|
+
known = readInstalledPackage(this.host, directory, nameSegments.join('/'));
|
|
2600
|
+
this.packageDirectories.set(directory, known);
|
|
2601
|
+
}
|
|
2602
|
+
return known ?? undefined;
|
|
2603
|
+
}
|
|
2604
|
+
/** Typed so that nothing can be passed: `never`, or only `undefined`. */
|
|
2605
|
+
isAbsent(property) {
|
|
2606
|
+
return this.parts(this.checker.getTypeOfSymbol(property)).every(part => (part.flags & ts.TypeFlags.Never) !== 0);
|
|
2607
|
+
}
|
|
2608
|
+
isBuiltinMember(property) {
|
|
2609
|
+
const declarations = property.declarations ?? [];
|
|
2610
|
+
return (declarations.length > 0 &&
|
|
2611
|
+
declarations.every(declaration => {
|
|
2612
|
+
const owner = declaration.parent;
|
|
2613
|
+
if (!owner || !ts.isInterfaceDeclaration(owner))
|
|
2614
|
+
return false;
|
|
2615
|
+
const symbol = this.checker.getSymbolAtLocation(owner.name);
|
|
2616
|
+
return symbol !== undefined && BUILTIN_INTERFACES.has(this.checker.getFullyQualifiedName(symbol));
|
|
2617
|
+
}));
|
|
2618
|
+
}
|
|
2619
|
+
/**
|
|
2620
|
+
* `string` is assignable to the type, and some part of it besides null and
|
|
2621
|
+
* undefined is string-like. `any`, `unknown`, `{}` and `Object` accept a
|
|
2622
|
+
* string without saying anything about one.
|
|
2623
|
+
*/
|
|
2624
|
+
acceptsString(declared) {
|
|
2625
|
+
const slot = this.throughConstraint(declared);
|
|
2626
|
+
if (slot === VARIADIC)
|
|
2627
|
+
return false;
|
|
2628
|
+
return (this.checker.isTypeAssignableTo(this.checker.getStringType(), slot) &&
|
|
2629
|
+
this.parts(slot).some(part => isStringLike(part)));
|
|
2630
|
+
}
|
|
2631
|
+
/** Accepts string, or names an HTTP method as a literal in either case. */
|
|
2632
|
+
acceptsMethod(declared) {
|
|
2633
|
+
const slot = this.throughConstraint(declared);
|
|
2634
|
+
if (slot === VARIADIC)
|
|
2635
|
+
return false;
|
|
2636
|
+
if (this.acceptsString(slot))
|
|
2637
|
+
return true;
|
|
2638
|
+
return this.parts(slot).some(part => part.isStringLiteral() && HTTP_METHODS.has(asciiUpperCase(part.value)));
|
|
2639
|
+
}
|
|
2640
|
+
/**
|
|
2641
|
+
* A body parameter that takes any payload: `unknown`, or a type parameter
|
|
2642
|
+
* with no constraint (or one of `unknown` or `any`). `any` itself is not
|
|
2643
|
+
* open: a declared `any`, an unresolved type and a defaulted type argument
|
|
2644
|
+
* all read as `any`, and none of them says the parameter is a body.
|
|
2645
|
+
*/
|
|
2646
|
+
isOpenBody(slot) {
|
|
2647
|
+
if (slot === VARIADIC)
|
|
2648
|
+
return false;
|
|
2649
|
+
const parts = this.parts(slot);
|
|
2650
|
+
return (parts.length > 0 &&
|
|
2651
|
+
parts.every(part => (part.flags & ts.TypeFlags.Unknown) !== 0 || this.isUnconstrained(part)));
|
|
2652
|
+
}
|
|
2653
|
+
/** Every part besides null and undefined is an object type. */
|
|
2654
|
+
isObjectType(slot) {
|
|
2655
|
+
if (slot === VARIADIC)
|
|
2656
|
+
return false;
|
|
2657
|
+
const parts = this.parts(slot);
|
|
2658
|
+
return parts.length > 0 && parts.every(part => this.isObjectLike(part));
|
|
2659
|
+
}
|
|
2660
|
+
isObjectLike(type) {
|
|
2661
|
+
if (type.flags & ts.TypeFlags.Object)
|
|
2662
|
+
return true;
|
|
2663
|
+
if (type.isIntersection())
|
|
2664
|
+
return type.types.every(part => this.isObjectLike(part));
|
|
2665
|
+
if (type.flags & ts.TypeFlags.TypeParameter) {
|
|
2666
|
+
const constraint = this.checker.getBaseConstraintOfType(type);
|
|
2667
|
+
return constraint !== undefined && constraint !== type && this.isObjectLike(constraint);
|
|
2668
|
+
}
|
|
2669
|
+
return false;
|
|
2670
|
+
}
|
|
2671
|
+
/**
|
|
2672
|
+
* The verdict when a slot fails its predicate: `unchecked` (`member_untyped`)
|
|
2673
|
+
* when its type says nothing, `failed` with `code` when it says something
|
|
2674
|
+
* else.
|
|
2675
|
+
*/
|
|
2676
|
+
slotFailure(slot, code) {
|
|
2677
|
+
return this.saysNothing(slot) ? unchecked('member_untyped') : failed(code);
|
|
2678
|
+
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Some part of the type, besides null and undefined, is `any`, `unknown`,
|
|
2681
|
+
* an unconstrained type parameter, or an object type with nothing declared
|
|
2682
|
+
* on it (`{}`, `Object`, `Function`).
|
|
2683
|
+
*/
|
|
2684
|
+
saysNothing(slot) {
|
|
2685
|
+
if (slot === VARIADIC)
|
|
2686
|
+
return true;
|
|
2687
|
+
return this.parts(slot).some(part => this.isOpenTop(part) || this.isUnconstrained(part) || this.isEmptyObject(part));
|
|
2688
|
+
}
|
|
2689
|
+
/**
|
|
2690
|
+
* What a factory builds says nothing: the type itself, or any branch of a
|
|
2691
|
+
* conditional return type, does.
|
|
2692
|
+
*/
|
|
2693
|
+
returnSaysNothing(type) {
|
|
2694
|
+
if (this.saysNothing(type))
|
|
2695
|
+
return true;
|
|
2696
|
+
return this.parts(type).some(part => {
|
|
2697
|
+
if (!(part.flags & ts.TypeFlags.Conditional))
|
|
2698
|
+
return false;
|
|
2699
|
+
const node = part.root.node;
|
|
2700
|
+
return [node.trueType, node.falseType].some(branch => this.returnSaysNothing(this.checker.getTypeFromTypeNode(branch)));
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
/** `any` (an unresolved type included) or `unknown`. */
|
|
2704
|
+
isOpenTop(type) {
|
|
2705
|
+
return (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0;
|
|
2706
|
+
}
|
|
2707
|
+
isUnconstrained(type) {
|
|
2708
|
+
if (!(type.flags & ts.TypeFlags.TypeParameter))
|
|
2709
|
+
return false;
|
|
2710
|
+
const constraint = this.checker.getBaseConstraintOfType(type);
|
|
2711
|
+
return constraint === undefined || this.isOpenTop(constraint);
|
|
2712
|
+
}
|
|
2713
|
+
isEmptyObject(type) {
|
|
2714
|
+
return ((type.flags & ts.TypeFlags.Object) !== 0 &&
|
|
2715
|
+
this.declaredProperties(type).length === 0 &&
|
|
2716
|
+
type.getCallSignatures().length === 0 &&
|
|
2717
|
+
type.getConstructSignatures().length === 0 &&
|
|
2718
|
+
this.checker.getIndexInfosOfType(type).length === 0);
|
|
2719
|
+
}
|
|
2720
|
+
/** The type's parts besides null and undefined. */
|
|
2721
|
+
parts(type) {
|
|
2722
|
+
return (type.isUnion() ? type.types : [type]).filter(part => !isNullish(part));
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* What the signature has at parameter `index`, reading through a rest
|
|
2726
|
+
* parameter; `undefined` when it has none there. A rest typed by a type
|
|
2727
|
+
* parameter (`...rest: A`) is unreadable, unless `readConstraint` asks for
|
|
2728
|
+
* its element through the constraint (`A extends Array<X>` reads `X`).
|
|
2729
|
+
*/
|
|
2730
|
+
parameterAt(signature, index, readConstraint = false) {
|
|
2731
|
+
const parameters = signature.getParameters();
|
|
2732
|
+
const last = parameters[parameters.length - 1];
|
|
2733
|
+
const declaration = last?.valueDeclaration;
|
|
2734
|
+
const restIndex = declaration && ts.isParameter(declaration) && declaration.dotDotDotToken
|
|
2735
|
+
? parameters.length - 1
|
|
2736
|
+
: -1;
|
|
2737
|
+
if (restIndex === -1 || index < restIndex) {
|
|
2738
|
+
return index < parameters.length ? this.checker.getTypeOfSymbol(parameters[index]) : undefined;
|
|
2739
|
+
}
|
|
2740
|
+
let rest = this.checker.getTypeOfSymbol(last);
|
|
2741
|
+
if (readConstraint && rest.flags & ts.TypeFlags.TypeParameter) {
|
|
2742
|
+
rest = this.checker.getBaseConstraintOfType(rest) ?? rest;
|
|
2743
|
+
}
|
|
2744
|
+
if (this.checker.isTupleType(rest)) {
|
|
2745
|
+
return this.checker.getTypeArguments(rest)[index - restIndex];
|
|
2746
|
+
}
|
|
2747
|
+
if (this.checker.isArrayType(rest)) {
|
|
2748
|
+
return this.checker.getTypeArguments(rest)[0];
|
|
2749
|
+
}
|
|
2750
|
+
return this.isOpenTop(rest) ? rest : VARIADIC;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
/**
|
|
2754
|
+
* What `typeToString` asks the node builder for, less its length cut: a
|
|
2755
|
+
* union the builder cuts short keeps the members it made first, so the
|
|
2756
|
+
* listing orders every union before its own cut (`truncate`).
|
|
2757
|
+
*/
|
|
2758
|
+
const PRINT_FLAGS = ts.NodeBuilderFlags.AllowUniqueESSymbolType |
|
|
2759
|
+
ts.NodeBuilderFlags.UseAliasDefinedOutsideCurrentScope |
|
|
2760
|
+
ts.NodeBuilderFlags.IgnoreErrors |
|
|
2761
|
+
ts.NodeBuilderFlags.NoTruncation;
|
|
2762
|
+
const TYPE_PRINTER = ts.createPrinter({ removeComments: true });
|
|
2763
|
+
/**
|
|
2764
|
+
* A type node as `typeToString` prints it with no enclosing declaration: with
|
|
2765
|
+
* no source file, so a node the builder reused from a declaration prints its
|
|
2766
|
+
* own text.
|
|
2767
|
+
*/
|
|
2768
|
+
function printTypeNode(node) {
|
|
2769
|
+
return TYPE_PRINTER.printNode(ts.EmitHint.Unspecified, node, undefined);
|
|
2770
|
+
}
|
|
2771
|
+
/** Code-unit order on names. */
|
|
2772
|
+
function byName(a, b) {
|
|
2773
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
2774
|
+
}
|
|
2775
|
+
/**
|
|
2776
|
+
* Properties by name. A type's own property order is declaration order, but
|
|
2777
|
+
* a mapped type's (`Record<keyof M, F>`, `Partial<A & B>`) follows its key
|
|
2778
|
+
* union, in the order the compiler made the keys: listed as it comes, the
|
|
2779
|
+
* order would move with whatever the program read first.
|
|
2780
|
+
*/
|
|
2781
|
+
function sortedByName(properties) {
|
|
2782
|
+
return [...properties].sort((a, b) => byName(a.getName(), b.getName()));
|
|
2783
|
+
}
|
|
2784
|
+
/** A printed type, cut to a length a listing can carry. */
|
|
2785
|
+
function truncate(text) {
|
|
2786
|
+
return text.length > 200 ? `${text.slice(0, 197)}...` : text;
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* The package a specifier names (`@scope/name` or `name`, without a subpath)
|
|
2790
|
+
* is `packageName`, or `packageName` is its `@types` package
|
|
2791
|
+
* (`@types/scope__name` for a scoped one).
|
|
2792
|
+
*/
|
|
2793
|
+
function isNamedPackage(specifier, packageName) {
|
|
2794
|
+
const named = packageNameOf(specifier);
|
|
2795
|
+
return packageName === named || packageName === typesPackageOf(named);
|
|
2796
|
+
}
|
|
2797
|
+
/** The `@types` package of a package name (`@types/scope__name` for a scoped one). */
|
|
2798
|
+
function typesPackageOf(named) {
|
|
2799
|
+
return `@types/${named.startsWith('@') ? named.slice(1).replace('/', '__') : named}`;
|
|
2800
|
+
}
|
|
2801
|
+
/** The package a specifier names: `@scope/name` or `name`, without a subpath. */
|
|
2802
|
+
function packageNameOf(specifier) {
|
|
2803
|
+
const segments = specifier.split('/');
|
|
2804
|
+
return specifier.startsWith('@') ? segments.slice(0, 2).join('/') : segments[0];
|
|
2805
|
+
}
|
|
2806
|
+
function readInstalledPackage(host, directory, directoryName) {
|
|
2807
|
+
const manifest = path.join(directory, 'package.json');
|
|
2808
|
+
if (!host.fileExists(manifest))
|
|
2809
|
+
return null;
|
|
2810
|
+
let packageName;
|
|
2811
|
+
let version;
|
|
2812
|
+
try {
|
|
2813
|
+
const parsed = JSON.parse(host.readFile(manifest) ?? '');
|
|
2814
|
+
if (typeof parsed?.name === 'string')
|
|
2815
|
+
packageName = parsed.name;
|
|
2816
|
+
if (typeof parsed?.version === 'string')
|
|
2817
|
+
version = parsed.version;
|
|
2818
|
+
}
|
|
2819
|
+
catch {
|
|
2820
|
+
// An unreadable manifest still marks an installed directory; it names no package.
|
|
2821
|
+
}
|
|
2822
|
+
return { directoryName, packageName, version };
|
|
2823
|
+
}
|
|
2824
|
+
function isNullish(type) {
|
|
2825
|
+
return (type.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0;
|
|
2826
|
+
}
|
|
2827
|
+
/**
|
|
2828
|
+
* Keyed by a unique symbol. The checker escapes such a name as
|
|
2829
|
+
* `__@<description>@<symbol id>`; a string key that starts with `__` is
|
|
2830
|
+
* escaped with one more underscore, so it never matches.
|
|
2831
|
+
*/
|
|
2832
|
+
function isSymbolKeyed(property) {
|
|
2833
|
+
return property.escapedName.startsWith('__@');
|
|
2834
|
+
}
|
|
2835
|
+
/** `string`, a string literal or template, or an intersection with one (`string & {}`). */
|
|
2836
|
+
function isStringLike(type) {
|
|
2837
|
+
if (type.flags & ts.TypeFlags.StringLike)
|
|
2838
|
+
return true;
|
|
2839
|
+
return type.isIntersection() && type.types.some(isStringLike);
|
|
2840
|
+
}
|