@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.
Files changed (98) hide show
  1. package/dist/src/analysis.js +45 -32
  2. package/dist/src/component-layer.js +1 -1
  3. package/dist/src/configuration-files.js +227 -27
  4. package/dist/src/conformance-record.js +2 -2
  5. package/dist/src/contextual-uses.js +68 -10
  6. package/dist/src/convention-rows.js +48 -0
  7. package/dist/src/conventions.js +80 -0
  8. package/dist/src/dependencies.js +121 -46
  9. package/dist/src/discover.js +1 -1
  10. package/dist/src/edges.js +1 -1
  11. package/dist/src/exempt-classes.js +9 -22
  12. package/dist/src/exempt.js +3 -15
  13. package/dist/src/findings/completion.js +41 -11
  14. package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
  15. package/dist/src/findings/emitters.js +0 -4
  16. package/dist/src/findings/self-check.js +14 -10
  17. package/dist/src/html-entries.js +29 -14
  18. package/dist/src/indexed-access.js +377 -81
  19. package/dist/src/intra-function-parts.js +57 -11
  20. package/dist/src/inventory.js +3 -12
  21. package/dist/src/kinds.js +6 -6
  22. package/dist/src/references.js +62 -23
  23. package/dist/src/report.js +3 -0
  24. package/dist/src/roots.js +96 -7
  25. package/dist/src/run.js +0 -2
  26. package/dist/src/schema.js +1 -1
  27. package/dist/src/scope.js +2 -2
  28. package/dist/src/session.js +6 -2
  29. package/dist/src/source-files.js +1 -0
  30. package/dist/src/suppress-inline.js +1 -1
  31. package/dist/src/suppress.js +15 -16
  32. package/dist/src/sweep.js +29 -17
  33. package/dist/src/symbol-keys.js +114 -0
  34. package/dist/src/test-joins.js +101 -0
  35. package/dist/src/version.js +1 -1
  36. package/dist/src/workflow-steps.js +21 -15
  37. package/dist/src/workspace-programs.js +1 -4
  38. package/dist/src/workspace-resolution.js +0 -2
  39. package/dist/src/workspace.js +0 -3
  40. package/jsr.json +1 -1
  41. package/package.json +2 -1
  42. package/src/analysis.ts +55 -47
  43. package/src/calls.ts +3 -3
  44. package/src/component-files.ts +7 -7
  45. package/src/component-layer.ts +2 -2
  46. package/src/components.ts +1 -1
  47. package/src/config.ts +4 -4
  48. package/src/configuration-files.ts +246 -28
  49. package/src/conformance-record.ts +2 -2
  50. package/src/contextual-uses.ts +99 -13
  51. package/src/convention-rows.ts +76 -0
  52. package/src/conventions.ts +102 -0
  53. package/src/dependencies.ts +148 -56
  54. package/src/discover.ts +3 -3
  55. package/src/edges.ts +3 -3
  56. package/src/entry-points.ts +1 -1
  57. package/src/exempt-classes.ts +10 -24
  58. package/src/exempt.ts +5 -29
  59. package/src/file-facts.ts +1 -1
  60. package/src/finding.ts +5 -1
  61. package/src/findings/completion.ts +53 -11
  62. package/src/findings/dependencies-and-module-machinery.ts +9 -28
  63. package/src/findings/emitters.ts +3 -8
  64. package/src/findings/self-check.ts +17 -14
  65. package/src/findings-pass.ts +2 -2
  66. package/src/graph.ts +2 -2
  67. package/src/html-entries.ts +34 -16
  68. package/src/indexed-access.ts +476 -102
  69. package/src/intra-function-parts.ts +84 -12
  70. package/src/inventory.ts +5 -33
  71. package/src/json-document.ts +1 -1
  72. package/src/kinds.ts +7 -6
  73. package/src/manifest.ts +1 -1
  74. package/src/namespace-objects.ts +1 -1
  75. package/src/pnpm-workspace.ts +1 -1
  76. package/src/references.ts +93 -33
  77. package/src/report.ts +8 -4
  78. package/src/roots.ts +129 -9
  79. package/src/run.ts +0 -2
  80. package/src/schema.ts +2 -2
  81. package/src/scope.ts +2 -2
  82. package/src/session.ts +7 -3
  83. package/src/source-files.ts +3 -0
  84. package/src/stores.ts +1 -1
  85. package/src/suppress-inline.ts +3 -3
  86. package/src/suppress.ts +21 -24
  87. package/src/sweep.ts +29 -17
  88. package/src/symbol-keys.ts +172 -0
  89. package/src/template-field.ts +1 -1
  90. package/src/test-joins.ts +120 -0
  91. package/src/value-reach.ts +1 -1
  92. package/src/verbs/verb.ts +1 -4
  93. package/src/version.ts +1 -1
  94. package/src/workflow-steps.ts +23 -16
  95. package/src/workspace-programs.ts +3 -10
  96. package/src/workspace-resolution.ts +0 -3
  97. package/src/workspace-run.ts +1 -1
  98. 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
- export interface OutsideModule {
113
+ interface OutsideModule {
104
114
  /** The file, parsed alone. */
105
115
  readonly file: SourceFile;
106
- /** The directory of the configuration module the file was reached from, which holds its manifest. */
107
- readonly manifestDir: string;
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
- /** Every JSON configuration file in a directory of the project's files or above one. */
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 documents: ConfigurationDocument[] = [];
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
- for (const name of names) {
206
- const path = joinPath(dir, name);
207
- let value: unknown;
208
- try {
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
- documents.push({ path, strings: documentStrings(value) });
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 ? [] : outsideModules(host, [...dirs], files, holdsManifest, parse),
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: OutsideModule[] = [];
302
- const seen = new Set<string>();
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
- if (seen.has(next.path) || files.byName.has(next.path)) {
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
- seen.add(next.path);
308
- let text: string;
309
- try {
310
- text = host.readFile(next.path);
311
- } catch {
312
- continue;
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
- const file = parse(next.path, text);
315
- found.push({ file, manifestDir: next.manifestDir });
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: "4.1.0",
5
+ corpusVersion: "5.0.0",
6
6
  result: "pass",
7
- digest: "sha256:167f770cdff2776fa7ab28b7ff6121c4d7bdb01d83c5ba0057d9303281614008",
7
+ digest: "sha256:26c486907df44fb8eaf3f749f60a6010ac94e7cfeeda70c4aa406445193ab80a",
8
8
  },
9
9
  gaps: [],
10
10
  } as const;
@@ -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
- * function parameters and results too; a class instance there is the
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
- export interface ContextualUse {
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
- export interface ContextualUses {
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
- /** The name one literal property is written under, where the name is literal text. */
67
- function propertyName(property: Node): { node: Node; text: string } | undefined {
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
- if (name === undefined) {
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(name.text)?.ids ?? []))];
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
- /** The parameter and return types of a type's one call signature, read once per type. */
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
- const signatures = queries.signaturesOf(type, SignatureKind.Call);
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 === undefined) {
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
  }