@cplieger/deadset-ts 6.1.1-dev.1 → 7.0.0-dev.2
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/src/analysis.js +45 -32
- package/dist/src/component-layer.js +1 -1
- package/dist/src/configuration-files.js +227 -27
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +68 -10
- package/dist/src/convention-rows.js +48 -0
- package/dist/src/conventions.js +80 -0
- package/dist/src/dependencies.js +121 -46
- package/dist/src/discover.js +1 -1
- package/dist/src/edges.js +1 -1
- package/dist/src/exempt-classes.js +9 -22
- package/dist/src/exempt.js +3 -15
- package/dist/src/findings/completion.js +41 -11
- package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
- package/dist/src/findings/emitters.js +0 -4
- package/dist/src/findings/self-check.js +14 -10
- package/dist/src/html-entries.js +29 -14
- package/dist/src/indexed-access.js +377 -81
- package/dist/src/intra-function-parts.js +57 -11
- package/dist/src/inventory.js +3 -12
- package/dist/src/kinds.js +6 -6
- package/dist/src/references.js +62 -23
- package/dist/src/report.js +3 -0
- package/dist/src/roots.js +96 -7
- package/dist/src/run.js +0 -2
- package/dist/src/schema.js +1 -1
- package/dist/src/scope.js +2 -2
- package/dist/src/session.js +6 -2
- package/dist/src/source-files.js +1 -0
- package/dist/src/suppress-inline.js +1 -1
- package/dist/src/suppress.js +15 -16
- package/dist/src/sweep.js +29 -17
- package/dist/src/symbol-keys.js +114 -0
- package/dist/src/test-joins.js +101 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +21 -15
- package/dist/src/workspace-programs.js +1 -4
- package/dist/src/workspace-resolution.js +0 -2
- package/dist/src/workspace.js +0 -3
- package/jsr.json +1 -1
- package/package.json +2 -1
- package/src/analysis.ts +55 -47
- package/src/calls.ts +3 -3
- package/src/component-files.ts +7 -7
- package/src/component-layer.ts +2 -2
- package/src/components.ts +1 -1
- package/src/config.ts +4 -4
- package/src/configuration-files.ts +246 -28
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +99 -13
- package/src/convention-rows.ts +76 -0
- package/src/conventions.ts +102 -0
- package/src/dependencies.ts +148 -56
- package/src/discover.ts +3 -3
- package/src/edges.ts +3 -3
- package/src/entry-points.ts +1 -1
- package/src/exempt-classes.ts +10 -24
- package/src/exempt.ts +5 -29
- package/src/file-facts.ts +1 -1
- package/src/finding.ts +5 -1
- package/src/findings/completion.ts +53 -11
- package/src/findings/dependencies-and-module-machinery.ts +9 -28
- package/src/findings/emitters.ts +3 -8
- package/src/findings/self-check.ts +17 -14
- package/src/findings-pass.ts +2 -2
- package/src/graph.ts +2 -2
- package/src/html-entries.ts +34 -16
- package/src/indexed-access.ts +476 -102
- package/src/intra-function-parts.ts +84 -12
- package/src/inventory.ts +5 -33
- package/src/json-document.ts +1 -1
- package/src/kinds.ts +7 -6
- package/src/manifest.ts +1 -1
- package/src/namespace-objects.ts +1 -1
- package/src/pnpm-workspace.ts +1 -1
- package/src/references.ts +93 -33
- package/src/report.ts +8 -4
- package/src/roots.ts +129 -9
- package/src/run.ts +0 -2
- package/src/schema.ts +2 -2
- package/src/scope.ts +2 -2
- package/src/session.ts +7 -3
- package/src/source-files.ts +3 -0
- package/src/stores.ts +1 -1
- package/src/suppress-inline.ts +3 -3
- package/src/suppress.ts +21 -24
- package/src/sweep.ts +29 -17
- package/src/symbol-keys.ts +172 -0
- package/src/template-field.ts +1 -1
- package/src/test-joins.ts +120 -0
- package/src/value-reach.ts +1 -1
- package/src/verbs/verb.ts +1 -4
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +23 -16
- package/src/workspace-programs.ts +3 -10
- package/src/workspace-resolution.ts +0 -3
- package/src/workspace-run.ts +1 -1
- package/src/workspace.ts +1 -9
|
@@ -69,11 +69,19 @@ export const RESOLVED_EXTENSIONS = [
|
|
|
69
69
|
".cjs",
|
|
70
70
|
] as const;
|
|
71
71
|
|
|
72
|
+
/** The directory a package manager installs packages into, whose files are no project's own. */
|
|
73
|
+
const MODULES_DIR = "node_modules";
|
|
74
|
+
|
|
72
75
|
/** The file's name, the last segment of its path. */
|
|
73
76
|
function basename(path: string): string {
|
|
74
77
|
return path.slice(path.lastIndexOf("/") + 1);
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
/** Whether one path is the project's own: below the target root and in no `node_modules`. */
|
|
81
|
+
function isProjectPath(root: string, path: string): boolean {
|
|
82
|
+
return relativePath(root, path)?.split("/").includes(MODULES_DIR) === false;
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
78
86
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
87
|
}
|
|
@@ -92,6 +100,8 @@ export function isConfigurationDocumentName(name: string): boolean {
|
|
|
92
100
|
interface ConfigurationDocument {
|
|
93
101
|
/** The file, absolute. */
|
|
94
102
|
readonly path: string;
|
|
103
|
+
/** The directories of the manifests whose dependencies the strings are read against. */
|
|
104
|
+
readonly manifestDirs: readonly string[];
|
|
95
105
|
/** Every string value at any depth, in document order; a member's name is no value. */
|
|
96
106
|
readonly strings: readonly string[];
|
|
97
107
|
}
|
|
@@ -100,11 +110,169 @@ interface ConfigurationDocument {
|
|
|
100
110
|
export type ParseFile = (fileName: string, text: string) => SourceFile;
|
|
101
111
|
|
|
102
112
|
/** One file no program holds that a configuration module outside the program leads to. */
|
|
103
|
-
|
|
113
|
+
interface OutsideModule {
|
|
104
114
|
/** The file, parsed alone. */
|
|
105
115
|
readonly file: SourceFile;
|
|
106
|
-
/** The
|
|
107
|
-
readonly
|
|
116
|
+
/** The directories of the manifests of the configuration files the file was reached from. */
|
|
117
|
+
readonly manifestDirs: readonly string[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The flags a tool's command line names its configuration file with. */
|
|
121
|
+
const CONFIG_FLAGS: ReadonlySet<string> = new Set(["--config", "-c"]);
|
|
122
|
+
|
|
123
|
+
/** The extensions of a module a configuration flag may name. */
|
|
124
|
+
const MODULE_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"] as const;
|
|
125
|
+
|
|
126
|
+
/** The characters that end a word outside quotes and separate nothing: blanks. */
|
|
127
|
+
const BLANK = /[ \t\r\f\v]/u;
|
|
128
|
+
|
|
129
|
+
/** The characters that end a word outside quotes and begin an operator: newline included. */
|
|
130
|
+
const OPERATOR = /[\n;&|()<>]/u;
|
|
131
|
+
|
|
132
|
+
/** The characters a backslash escapes inside double quotes; before any other it stands for itself. */
|
|
133
|
+
const DOUBLE_QUOTE_ESCAPES = new Set(["$", "`", '"', "\\"]);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The words of one command line, as POSIX token recognition reads quotes, backslashes,
|
|
137
|
+
* line continuations, comments and operators. A word an expansion may change (an unquoted
|
|
138
|
+
* `$`, backquote, glob character or leading `~`, or a `$` or backquote in double quotes)
|
|
139
|
+
* is computed, and so is the rest of a line whose quote is never closed. A computed word
|
|
140
|
+
* and each operator are undefined, so a flag's file is never read across them.
|
|
141
|
+
*/
|
|
142
|
+
function shellWords(command: string): readonly (string | undefined)[] {
|
|
143
|
+
const words: (string | undefined)[] = [];
|
|
144
|
+
let word: string | undefined;
|
|
145
|
+
let computed = false;
|
|
146
|
+
const end = (): void => {
|
|
147
|
+
if (word !== undefined) {
|
|
148
|
+
words.push(computed ? undefined : word);
|
|
149
|
+
}
|
|
150
|
+
word = undefined;
|
|
151
|
+
computed = false;
|
|
152
|
+
};
|
|
153
|
+
for (let at = 0; at < command.length; at += 1) {
|
|
154
|
+
const char = command.charAt(at);
|
|
155
|
+
if (char === "\\" && command.charAt(at + 1) === "\n") {
|
|
156
|
+
at += 1;
|
|
157
|
+
} else if (BLANK.test(char)) {
|
|
158
|
+
end();
|
|
159
|
+
} else if (OPERATOR.test(char)) {
|
|
160
|
+
end();
|
|
161
|
+
words.push(undefined);
|
|
162
|
+
} else if (char === "#" && word === undefined) {
|
|
163
|
+
const newline = command.indexOf("\n", at);
|
|
164
|
+
at = newline < 0 ? command.length : newline - 1;
|
|
165
|
+
} else if (char === "\\") {
|
|
166
|
+
at += 1;
|
|
167
|
+
word = (word ?? "") + command.charAt(at);
|
|
168
|
+
} else if (char === "'" || char === '"') {
|
|
169
|
+
const close = closingQuote(command, at);
|
|
170
|
+
if (close < 0) {
|
|
171
|
+
word ??= "";
|
|
172
|
+
computed = true;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
const quoted =
|
|
176
|
+
char === "'"
|
|
177
|
+
? { text: command.slice(at + 1, close), computed: false }
|
|
178
|
+
: doubleQuoted(command.slice(at + 1, close));
|
|
179
|
+
word = (word ?? "") + quoted.text;
|
|
180
|
+
computed ||= quoted.computed;
|
|
181
|
+
at = close;
|
|
182
|
+
} else {
|
|
183
|
+
computed ||= /[$`*?[]/u.test(char) || (char === "~" && word === undefined);
|
|
184
|
+
word = (word ?? "") + char;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
end();
|
|
188
|
+
return words;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** The text between one pair of double quotes, unescaped, and whether an expansion may change it. */
|
|
192
|
+
function doubleQuoted(inner: string): { readonly text: string; readonly computed: boolean } {
|
|
193
|
+
let text = "";
|
|
194
|
+
let computed = false;
|
|
195
|
+
for (let at = 0; at < inner.length; at += 1) {
|
|
196
|
+
const char = inner.charAt(at);
|
|
197
|
+
const next = inner.charAt(at + 1);
|
|
198
|
+
if (char === "\\" && next === "\n") {
|
|
199
|
+
at += 1;
|
|
200
|
+
} else if (char === "\\" && DOUBLE_QUOTE_ESCAPES.has(next)) {
|
|
201
|
+
text += next;
|
|
202
|
+
at += 1;
|
|
203
|
+
} else {
|
|
204
|
+
computed ||= char === "$" || char === "`";
|
|
205
|
+
text += char;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { text, computed };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The index of the quote that closes the one at `open`, or -1 where none does. */
|
|
212
|
+
function closingQuote(command: string, open: number): number {
|
|
213
|
+
const quote = command.charAt(open);
|
|
214
|
+
for (let at = open + 1; at < command.length; at += 1) {
|
|
215
|
+
const char = command.charAt(at);
|
|
216
|
+
if (char === quote) {
|
|
217
|
+
return at;
|
|
218
|
+
}
|
|
219
|
+
if (quote === '"' && char === "\\") {
|
|
220
|
+
at += 1;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return -1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The files the scripts of the manifest in one directory name after a configuration flag,
|
|
228
|
+
* `--config <file>`, `-c <file>` or `--config=<file>`, each a JSON file or a module below
|
|
229
|
+
* the target root and in no `node_modules`.
|
|
230
|
+
*/
|
|
231
|
+
function scriptConfigurations(host: Host, root: string, dir: string): readonly string[] {
|
|
232
|
+
let scripts: unknown;
|
|
233
|
+
try {
|
|
234
|
+
const manifest: unknown = JSON.parse(host.readFile(joinPath(dir, MANIFEST)));
|
|
235
|
+
scripts = isRecord(manifest) ? manifest["scripts"] : undefined;
|
|
236
|
+
} catch {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
if (!isRecord(scripts)) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
const found: string[] = [];
|
|
243
|
+
for (const command of Object.values(scripts)) {
|
|
244
|
+
if (typeof command !== "string") {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const words = shellWords(command);
|
|
248
|
+
words.forEach((word, at) => {
|
|
249
|
+
const named =
|
|
250
|
+
word !== undefined && CONFIG_FLAGS.has(word)
|
|
251
|
+
? words[at + 1]
|
|
252
|
+
: word === undefined
|
|
253
|
+
? undefined
|
|
254
|
+
: /^--config=(.+)$/su.exec(word)?.[1];
|
|
255
|
+
const path = named === undefined ? undefined : resolvePath(dir, named);
|
|
256
|
+
if (
|
|
257
|
+
path !== undefined &&
|
|
258
|
+
[".json", ...MODULE_EXTENSIONS].some((extension) => path.endsWith(extension)) &&
|
|
259
|
+
isProjectPath(root, path) &&
|
|
260
|
+
host.kindOf(path) === "file"
|
|
261
|
+
) {
|
|
262
|
+
found.push(path);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return found;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The decoded value of one JSON file, or undefined where it cannot be read or parsed. */
|
|
270
|
+
function readDocument(host: Host, path: string): unknown {
|
|
271
|
+
try {
|
|
272
|
+
return JSON.parse(host.readFile(path)) as unknown;
|
|
273
|
+
} catch {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
108
276
|
}
|
|
109
277
|
|
|
110
278
|
/** The configuration files one project holds and sits beside. */
|
|
@@ -117,7 +285,12 @@ interface ConfigurationFiles {
|
|
|
117
285
|
* the program does not hold either, read where a parser is given.
|
|
118
286
|
*/
|
|
119
287
|
readonly outside: readonly OutsideModule[];
|
|
120
|
-
/**
|
|
288
|
+
/**
|
|
289
|
+
* Every JSON configuration file in a directory of the project's files or above one, every
|
|
290
|
+
* JSON file a script's configuration flag names, and every JSON file below the target
|
|
291
|
+
* root and in no `node_modules` a relative string of either names, read against each
|
|
292
|
+
* manifest whose chain reaches it.
|
|
293
|
+
*/
|
|
121
294
|
readonly documents: readonly ConfigurationDocument[];
|
|
122
295
|
}
|
|
123
296
|
|
|
@@ -187,7 +360,8 @@ export function configurationFiles(
|
|
|
187
360
|
}
|
|
188
361
|
}
|
|
189
362
|
|
|
190
|
-
const
|
|
363
|
+
const pending: { path: string; manifestDir: string }[] = [];
|
|
364
|
+
const named: { path: string; manifestDir: string }[] = [];
|
|
191
365
|
for (const dir of [...dirs].sort()) {
|
|
192
366
|
if (!holdsManifest(dir)) {
|
|
193
367
|
continue;
|
|
@@ -200,24 +374,59 @@ export function configurationFiles(
|
|
|
200
374
|
.map((entry) => entry.name)
|
|
201
375
|
.sort();
|
|
202
376
|
} catch {
|
|
377
|
+
names = [];
|
|
378
|
+
}
|
|
379
|
+
pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
|
|
380
|
+
for (const path of scriptConfigurations(host, root, dir)) {
|
|
381
|
+
const held = files.byName.get(path);
|
|
382
|
+
if (path.endsWith(".json")) {
|
|
383
|
+
pending.push({ path, manifestDir: dir });
|
|
384
|
+
} else if (held !== undefined) {
|
|
385
|
+
modules.push({ file: held, form: "a script's configuration flag" });
|
|
386
|
+
} else {
|
|
387
|
+
named.push({ path, manifestDir: dir });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// A document reached from two manifests is read against each, so a chain is keyed by both.
|
|
393
|
+
const documents = new Map<string, { path: string; manifestDirs: string[]; strings: string[] }>();
|
|
394
|
+
const unreadable = new Set<string>();
|
|
395
|
+
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
396
|
+
let document = documents.get(next.path);
|
|
397
|
+
if (unreadable.has(next.path) || document?.manifestDirs.includes(next.manifestDir) === true) {
|
|
203
398
|
continue;
|
|
204
399
|
}
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
value = JSON.parse(host.readFile(path));
|
|
210
|
-
} catch {
|
|
400
|
+
if (document === undefined) {
|
|
401
|
+
const value = readDocument(host, next.path);
|
|
402
|
+
if (value === undefined) {
|
|
403
|
+
unreadable.add(next.path);
|
|
211
404
|
continue;
|
|
212
405
|
}
|
|
213
|
-
|
|
406
|
+
document = { path: next.path, manifestDirs: [], strings: documentStrings(value) };
|
|
407
|
+
documents.set(next.path, document);
|
|
408
|
+
}
|
|
409
|
+
document.manifestDirs.push(next.manifestDir);
|
|
410
|
+
for (const text of document.strings) {
|
|
411
|
+
const path = RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix))
|
|
412
|
+
? resolvePath(dirnamePath(next.path), text)
|
|
413
|
+
: undefined;
|
|
414
|
+
if (
|
|
415
|
+
path?.endsWith(".json") === true &&
|
|
416
|
+
isProjectPath(root, path) &&
|
|
417
|
+
host.kindOf(path) === "file"
|
|
418
|
+
) {
|
|
419
|
+
pending.push({ path, manifestDir: next.manifestDir });
|
|
420
|
+
}
|
|
214
421
|
}
|
|
215
422
|
}
|
|
216
423
|
return {
|
|
217
424
|
modules,
|
|
218
|
-
documents,
|
|
425
|
+
documents: [...documents.values()],
|
|
219
426
|
outside:
|
|
220
|
-
parse === undefined
|
|
427
|
+
parse === undefined
|
|
428
|
+
? []
|
|
429
|
+
: outsideModules(host, [...dirs], files, holdsManifest, parse, named),
|
|
221
430
|
};
|
|
222
431
|
}
|
|
223
432
|
|
|
@@ -280,8 +489,9 @@ function outsideModules(
|
|
|
280
489
|
files: SourceFiles,
|
|
281
490
|
holdsManifest: (dir: string) => boolean,
|
|
282
491
|
parse: ParseFile,
|
|
492
|
+
named: readonly { readonly path: string; readonly manifestDir: string }[],
|
|
283
493
|
): readonly OutsideModule[] {
|
|
284
|
-
const pending: { path: string; manifestDir: string }[] = [];
|
|
494
|
+
const pending: { path: string; manifestDir: string }[] = [...named];
|
|
285
495
|
for (const dir of [...dirs].sort()) {
|
|
286
496
|
if (!holdsManifest(dir)) {
|
|
287
497
|
continue;
|
|
@@ -298,29 +508,37 @@ function outsideModules(
|
|
|
298
508
|
}
|
|
299
509
|
pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
|
|
300
510
|
}
|
|
301
|
-
const found:
|
|
302
|
-
const
|
|
511
|
+
const found = new Map<string, { file: SourceFile; manifestDirs: string[] }>();
|
|
512
|
+
const unreadable = new Set<string>();
|
|
303
513
|
for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
|
|
304
|
-
|
|
514
|
+
let outside = found.get(next.path);
|
|
515
|
+
if (
|
|
516
|
+
unreadable.has(next.path) ||
|
|
517
|
+
files.byName.has(next.path) ||
|
|
518
|
+
outside?.manifestDirs.includes(next.manifestDir) === true
|
|
519
|
+
) {
|
|
305
520
|
continue;
|
|
306
521
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
522
|
+
if (outside === undefined) {
|
|
523
|
+
let text: string;
|
|
524
|
+
try {
|
|
525
|
+
text = host.readFile(next.path);
|
|
526
|
+
} catch {
|
|
527
|
+
unreadable.add(next.path);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
outside = { file: parse(next.path, text), manifestDirs: [] };
|
|
531
|
+
found.set(next.path, outside);
|
|
313
532
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
for (const specifier of relativeSpecifiers(file)) {
|
|
533
|
+
outside.manifestDirs.push(next.manifestDir);
|
|
534
|
+
for (const specifier of relativeSpecifiers(outside.file)) {
|
|
317
535
|
const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
|
|
318
536
|
if (reached !== undefined) {
|
|
319
537
|
pending.push({ path: reached, manifestDir: next.manifestDir });
|
|
320
538
|
}
|
|
321
539
|
}
|
|
322
540
|
}
|
|
323
|
-
return found;
|
|
541
|
+
return [...found.values()];
|
|
324
542
|
}
|
|
325
543
|
|
|
326
544
|
/** Every dependency the manifest in one directory declares, or none where it holds none to read. */
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
|
-
corpusVersion: "
|
|
5
|
+
corpusVersion: "5.0.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:26c486907df44fb8eaf3f749f60a6010ac94e7cfeeda70c4aa406445193ab80a",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
} as const;
|
package/src/contextual-uses.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* An object-literal property writes the member of its name on each constituent of the
|
|
4
4
|
* literal's contextual type that declares one. A value reaching a position typed by another
|
|
5
5
|
* object type reads each of its members that type declares, through array elements and
|
|
6
|
-
*
|
|
7
|
-
* interface-satisfaction class's evidence instead.
|
|
6
|
+
* the parameters and results of functions and constructors too; a class instance there is
|
|
7
|
+
* the interface-satisfaction class's evidence instead.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
@@ -18,12 +18,13 @@ import {
|
|
|
18
18
|
isPropertyAssignment,
|
|
19
19
|
isSetAccessorDeclaration,
|
|
20
20
|
isShorthandPropertyAssignment,
|
|
21
|
+
isSpreadAssignment,
|
|
21
22
|
isStringLiteral,
|
|
22
23
|
type Expression,
|
|
23
24
|
type Node,
|
|
24
25
|
type ObjectLiteralExpression,
|
|
25
26
|
} from "@typescript/native/unstable/ast";
|
|
26
|
-
import { SignatureKind, type Type } from "@typescript/native/unstable/sync";
|
|
27
|
+
import { SignatureKind, SymbolFlags, type Type } from "@typescript/native/unstable/sync";
|
|
27
28
|
import type { AliasChains } from "./alias-chain.ts";
|
|
28
29
|
import type { Inventory, SymbolKind } from "./inventory.ts";
|
|
29
30
|
import { isAnswered, type PropertyTables } from "./query.ts";
|
|
@@ -45,14 +46,14 @@ export interface FlowAt {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/** One use the contextual types make: the node it is written at and the members it names. */
|
|
48
|
-
|
|
49
|
+
interface ContextualUse {
|
|
49
50
|
readonly at: Node;
|
|
50
51
|
readonly from: string;
|
|
51
52
|
readonly ids: readonly string[];
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/** What the contextual types of one project's literals and values use, and what asking cost. */
|
|
55
|
-
|
|
56
|
+
interface ContextualUses {
|
|
56
57
|
/** Each literal property that writes a member, by the property's node. */
|
|
57
58
|
readonly writes: readonly ContextualUse[];
|
|
58
59
|
/** Each value whose members a position typed by another object type reads. */
|
|
@@ -63,8 +64,13 @@ export interface ContextualUses {
|
|
|
63
64
|
readonly lookups: number;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* The name one literal property is written under: its literal text, or the computed name
|
|
69
|
+
* node whose key the checker names, a unique symbol among them.
|
|
70
|
+
*/
|
|
71
|
+
function propertyName(
|
|
72
|
+
property: Node,
|
|
73
|
+
): { node: Node; text: string } | { node: Node; text: undefined } | undefined {
|
|
68
74
|
if (
|
|
69
75
|
!isPropertyAssignment(property) &&
|
|
70
76
|
!isShorthandPropertyAssignment(property) &&
|
|
@@ -83,12 +89,14 @@ function propertyName(property: Node): { node: Node; text: string } | undefined
|
|
|
83
89
|
if (isStringLiteral(key) || isNumericLiteral(key) || isNoSubstitutionTemplateLiteral(key)) {
|
|
84
90
|
return { node: name, text: key.text };
|
|
85
91
|
}
|
|
92
|
+
return { node: name, text: undefined };
|
|
86
93
|
}
|
|
87
94
|
return undefined;
|
|
88
95
|
}
|
|
89
96
|
|
|
90
|
-
/** The parameter and result types of one call signature, each where the checker answered. */
|
|
97
|
+
/** The parameter and result types of one call or construct signature, each where the checker answered. */
|
|
91
98
|
interface Called {
|
|
99
|
+
readonly kind: SignatureKind;
|
|
92
100
|
readonly parameters: readonly (Type | undefined)[];
|
|
93
101
|
readonly result: Type | undefined;
|
|
94
102
|
}
|
|
@@ -110,6 +118,8 @@ interface Member {
|
|
|
110
118
|
readonly handles: ReadonlySet<string>;
|
|
111
119
|
/** Whether a declaration of it is in one of the target's own files. */
|
|
112
120
|
readonly own: boolean;
|
|
121
|
+
/** Whether the checker reads it as an accessor. */
|
|
122
|
+
readonly accessor: boolean;
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
/**
|
|
@@ -129,6 +139,11 @@ export function contextualUses<Brand>(
|
|
|
129
139
|
const queries = project.queries;
|
|
130
140
|
const ownPaths = project.ownPaths();
|
|
131
141
|
const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
|
|
142
|
+
const privateNames = new Set(
|
|
143
|
+
held.symbols
|
|
144
|
+
.filter((symbol) => symbol.visibility === "private-name")
|
|
145
|
+
.map((symbol) => symbol.id),
|
|
146
|
+
);
|
|
132
147
|
let batches = 0;
|
|
133
148
|
let lookups = 0;
|
|
134
149
|
const batchesOf = (count: number): number => Math.ceil(count / cap);
|
|
@@ -171,6 +186,7 @@ export function contextualUses<Brand>(
|
|
|
171
186
|
property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`),
|
|
172
187
|
),
|
|
173
188
|
own: property.declarations.some((handle) => ownPaths.has(handle.path)),
|
|
189
|
+
accessor: (property.flags & SymbolFlags.Accessor) !== 0,
|
|
174
190
|
});
|
|
175
191
|
}
|
|
176
192
|
tables.set(type.id, table);
|
|
@@ -183,6 +199,31 @@ export function contextualUses<Brand>(
|
|
|
183
199
|
literals.map((one) => one.literal),
|
|
184
200
|
cap,
|
|
185
201
|
);
|
|
202
|
+
// A computed name that is not literal text names the property the literal declares
|
|
203
|
+
// under the checker's own key, which is how a type declares a member a unique symbol
|
|
204
|
+
// keys: asked once, for the literals that have a contextual type.
|
|
205
|
+
const computed = literals.flatMap(({ literal }, index) =>
|
|
206
|
+
contexts[index] === undefined || !isAnswered(contexts[index])
|
|
207
|
+
? []
|
|
208
|
+
: literal.properties.flatMap((property) => {
|
|
209
|
+
const name = propertyName(property);
|
|
210
|
+
return name !== undefined && name.text === undefined ? [name.node] : [];
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
batches += batchesOf(computed.length);
|
|
214
|
+
const keys = new Map<Node, string>();
|
|
215
|
+
(computed.length === 0
|
|
216
|
+
? []
|
|
217
|
+
: project.symbolsAt(
|
|
218
|
+
computed.map((node) => project.handle(node)),
|
|
219
|
+
cap,
|
|
220
|
+
)
|
|
221
|
+
).forEach((symbol, index) => {
|
|
222
|
+
const node = computed[index];
|
|
223
|
+
if (node !== undefined && symbol !== undefined && isAnswered(symbol)) {
|
|
224
|
+
keys.set(node, symbol.name);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
186
227
|
literals.forEach(({ literal, from }, index) => {
|
|
187
228
|
const context = contexts[index];
|
|
188
229
|
if (context === undefined || !isAnswered(context)) {
|
|
@@ -191,10 +232,11 @@ export function contextualUses<Brand>(
|
|
|
191
232
|
const tablesOf = partsOf(context).map(membersOf);
|
|
192
233
|
for (const property of literal.properties) {
|
|
193
234
|
const name = propertyName(property);
|
|
194
|
-
|
|
235
|
+
const text = name?.text ?? (name === undefined ? undefined : keys.get(name.node));
|
|
236
|
+
if (name === undefined || text === undefined) {
|
|
195
237
|
continue;
|
|
196
238
|
}
|
|
197
|
-
const ids = [...new Set(tablesOf.flatMap((table) => table.get(
|
|
239
|
+
const ids = [...new Set(tablesOf.flatMap((table) => table.get(text)?.ids ?? []))];
|
|
198
240
|
if (ids.length > 0) {
|
|
199
241
|
writes.push({ at: name.node, from, ids });
|
|
200
242
|
}
|
|
@@ -237,13 +279,22 @@ export function contextualUses<Brand>(
|
|
|
237
279
|
};
|
|
238
280
|
|
|
239
281
|
const calls = new Map<number, Called | undefined>();
|
|
240
|
-
/**
|
|
282
|
+
/**
|
|
283
|
+
* The parameter and return types of a type's one call signature, or of its one construct
|
|
284
|
+
* signature where it has no call signature, read once per type.
|
|
285
|
+
*/
|
|
241
286
|
const callOf = (type: Type): Called | undefined => {
|
|
242
287
|
if (calls.has(type.id)) {
|
|
243
288
|
return calls.get(type.id);
|
|
244
289
|
}
|
|
245
290
|
lookups += 1;
|
|
246
|
-
|
|
291
|
+
let kind = SignatureKind.Call;
|
|
292
|
+
let signatures = queries.signaturesOf(type, kind);
|
|
293
|
+
if (isAnswered(signatures) && signatures.length === 0) {
|
|
294
|
+
lookups += 1;
|
|
295
|
+
kind = SignatureKind.Construct;
|
|
296
|
+
signatures = queries.signaturesOf(type, kind);
|
|
297
|
+
}
|
|
247
298
|
let called: Called | undefined;
|
|
248
299
|
const [signature, ...more] = isAnswered(signatures) ? signatures : [];
|
|
249
300
|
if (signature !== undefined && more.length === 0) {
|
|
@@ -253,6 +304,7 @@ export function contextualUses<Brand>(
|
|
|
253
304
|
const result = queries.returnTypeOf(signature);
|
|
254
305
|
lookups += parameters.length + 1;
|
|
255
306
|
called = {
|
|
307
|
+
kind,
|
|
256
308
|
parameters: parameters.map((one) => (isAnswered(one) ? one : undefined)),
|
|
257
309
|
result: isAnswered(result) ? result : undefined,
|
|
258
310
|
};
|
|
@@ -298,7 +350,7 @@ export function contextualUses<Brand>(
|
|
|
298
350
|
continue;
|
|
299
351
|
}
|
|
300
352
|
const other = called === undefined ? undefined : callOf(part);
|
|
301
|
-
if (called === undefined || other
|
|
353
|
+
if (called === undefined || other?.kind !== called.kind) {
|
|
302
354
|
continue;
|
|
303
355
|
}
|
|
304
356
|
other.parameters.forEach((parameter, index) => {
|
|
@@ -381,5 +433,39 @@ export function contextualUses<Brand>(
|
|
|
381
433
|
reads.push({ at: flow.value, from: flow.from, ids: [...ids] });
|
|
382
434
|
}
|
|
383
435
|
});
|
|
436
|
+
const spreads = literals.flatMap(({ literal, from }) =>
|
|
437
|
+
literal.properties.flatMap((property) =>
|
|
438
|
+
isSpreadAssignment(property) ? [{ value: property.expression, from }] : [],
|
|
439
|
+
),
|
|
440
|
+
);
|
|
441
|
+
batches += batchesOf(spreads.length);
|
|
442
|
+
const spreadTypes = queries.typesAt(
|
|
443
|
+
spreads.map((one) => one.value),
|
|
444
|
+
cap,
|
|
445
|
+
);
|
|
446
|
+
spreads.forEach(({ value, from }, index) => {
|
|
447
|
+
const type = spreadTypes[index];
|
|
448
|
+
if (type === undefined || !isAnswered(type)) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
// A spread gets every own enumerable property, an overwritten key's getter included.
|
|
452
|
+
// So it reads each data member but a class's accessor, which sits on the prototype with
|
|
453
|
+
// the class's methods (never data members), and a private name, which is no property
|
|
454
|
+
// (https://tc39.es/ecma262/#sec-copydataproperties).
|
|
455
|
+
const own = (member: Member, id: string): boolean =>
|
|
456
|
+
!privateNames.has(id) && !(member.accessor && kinds.get(id) === "class-member");
|
|
457
|
+
const ids = [
|
|
458
|
+
...new Set(
|
|
459
|
+
partsOf(type).flatMap((part) =>
|
|
460
|
+
[...membersOf(part).values()].flatMap((member) =>
|
|
461
|
+
member.ids.filter((id) => own(member, id)),
|
|
462
|
+
),
|
|
463
|
+
),
|
|
464
|
+
),
|
|
465
|
+
];
|
|
466
|
+
if (ids.length > 0) {
|
|
467
|
+
reads.push({ at: value, from, ids });
|
|
468
|
+
}
|
|
469
|
+
});
|
|
384
470
|
return { writes, reads, batches, lookups };
|
|
385
471
|
}
|