@cplieger/deadset-ts 7.0.0-dev.1 → 7.0.0-dev.3

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 (113) hide show
  1. package/dist/src/analysis.js +95 -60
  2. package/dist/src/augmentations.js +14 -12
  3. package/dist/src/component-layer.js +1 -1
  4. package/dist/src/components.js +62 -20
  5. package/dist/src/configuration-files.js +230 -30
  6. package/dist/src/conformance-record.js +1 -1
  7. package/dist/src/contextual-uses.js +104 -13
  8. package/dist/src/convention-rows.js +10 -0
  9. package/dist/src/conventions.js +35 -1
  10. package/dist/src/dependencies.js +90 -32
  11. package/dist/src/discover.js +1 -1
  12. package/dist/src/edges.js +1 -1
  13. package/dist/src/element-access.js +341 -0
  14. package/dist/src/erased-stores.js +317 -0
  15. package/dist/src/exempt-classes.js +9 -22
  16. package/dist/src/exempt.js +3 -15
  17. package/dist/src/findings/completion.js +78 -16
  18. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  19. package/dist/src/findings/emitters.js +1 -5
  20. package/dist/src/findings/self-check.js +27 -10
  21. package/dist/src/findings-pass.js +6 -5
  22. package/dist/src/in-operands.js +30 -0
  23. package/dist/src/indexed-access.js +264 -119
  24. package/dist/src/intra-function-parts.js +25 -11
  25. package/dist/src/inventory.js +28 -12
  26. package/dist/src/keyof-comparisons.js +170 -0
  27. package/dist/src/kinds.js +17 -7
  28. package/dist/src/manifest.js +6 -16
  29. package/dist/src/matrix.js +20 -2
  30. package/dist/src/query.js +3 -0
  31. package/dist/src/references.js +14 -3
  32. package/dist/src/report.js +4 -1
  33. package/dist/src/reporters/sarif.js +4 -0
  34. package/dist/src/roots.js +0 -1
  35. package/dist/src/run.js +0 -2
  36. package/dist/src/schema.js +1 -1
  37. package/dist/src/scope.js +2 -2
  38. package/dist/src/serialization-contract.js +43 -14
  39. package/dist/src/session.js +12 -8
  40. package/dist/src/suppress-file.js +58 -6
  41. package/dist/src/suppress-inline.js +14 -5
  42. package/dist/src/suppress.js +64 -35
  43. package/dist/src/unimplemented-callees.js +312 -0
  44. package/dist/src/value-reach.js +7 -7
  45. package/dist/src/verbs/analyze.js +11 -9
  46. package/dist/src/workspace-programs.js +0 -3
  47. package/dist/src/workspace-resolution.js +0 -2
  48. package/dist/src/workspace.js +0 -3
  49. package/jsr.json +1 -1
  50. package/package.json +3 -2
  51. package/src/analysis.ts +145 -82
  52. package/src/augmentations.ts +14 -11
  53. package/src/calls.ts +3 -3
  54. package/src/component-files.ts +7 -7
  55. package/src/component-layer.ts +2 -2
  56. package/src/components.ts +104 -22
  57. package/src/config.ts +4 -4
  58. package/src/configuration-files.ts +250 -31
  59. package/src/conformance-record.ts +1 -1
  60. package/src/contextual-uses.ts +135 -17
  61. package/src/convention-rows.ts +23 -0
  62. package/src/conventions.ts +53 -1
  63. package/src/dependencies.ts +119 -38
  64. package/src/discover.ts +3 -3
  65. package/src/edges.ts +3 -3
  66. package/src/element-access.ts +446 -0
  67. package/src/entry-points.ts +1 -1
  68. package/src/erased-stores.ts +413 -0
  69. package/src/exempt-classes.ts +10 -24
  70. package/src/exempt.ts +5 -29
  71. package/src/file-facts.ts +3 -1
  72. package/src/finding.ts +3 -1
  73. package/src/findings/completion.ts +97 -21
  74. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  75. package/src/findings/emitters.ts +4 -9
  76. package/src/findings/self-check.ts +31 -14
  77. package/src/findings-pass.ts +14 -7
  78. package/src/graph.ts +2 -2
  79. package/src/html-entries.ts +1 -1
  80. package/src/in-operands.ts +45 -0
  81. package/src/indexed-access.ts +303 -137
  82. package/src/intra-function-parts.ts +41 -16
  83. package/src/inventory.ts +37 -33
  84. package/src/json-document.ts +1 -1
  85. package/src/keyof-comparisons.ts +211 -0
  86. package/src/kinds.ts +20 -7
  87. package/src/manifest.ts +7 -22
  88. package/src/matrix.ts +34 -2
  89. package/src/namespace-objects.ts +1 -1
  90. package/src/pnpm-workspace.ts +1 -1
  91. package/src/query.ts +10 -0
  92. package/src/references.ts +30 -7
  93. package/src/report.ts +11 -5
  94. package/src/reporters/sarif.ts +4 -0
  95. package/src/roots.ts +0 -3
  96. package/src/run.ts +0 -2
  97. package/src/schema.ts +2 -2
  98. package/src/scope.ts +2 -2
  99. package/src/serialization-contract.ts +53 -14
  100. package/src/session.ts +34 -31
  101. package/src/stores.ts +1 -1
  102. package/src/suppress-file.ts +68 -5
  103. package/src/suppress-inline.ts +17 -7
  104. package/src/suppress.ts +79 -47
  105. package/src/template-field.ts +1 -1
  106. package/src/unimplemented-callees.ts +411 -0
  107. package/src/value-reach.ts +12 -8
  108. package/src/verbs/analyze.ts +15 -9
  109. package/src/verbs/verb.ts +1 -4
  110. package/src/workspace-programs.ts +2 -9
  111. package/src/workspace-resolution.ts +0 -3
  112. package/src/workspace-run.ts +1 -1
  113. package/src/workspace.ts +1 -9
@@ -50,10 +50,16 @@ export const RESOLVED_EXTENSIONS = [
50
50
  ".mjs",
51
51
  ".cjs",
52
52
  ];
53
+ /** The directory a package manager installs packages into, whose files are no project's own. */
54
+ const MODULES_DIR = "node_modules";
53
55
  /** The file's name, the last segment of its path. */
54
56
  function basename(path) {
55
57
  return path.slice(path.lastIndexOf("/") + 1);
56
58
  }
59
+ /** Whether one path is the project's own: below the target root and in no `node_modules`. */
60
+ function isProjectPath(root, path) {
61
+ return relativePath(root, path)?.split("/").includes(MODULES_DIR) === false;
62
+ }
57
63
  function isRecord(value) {
58
64
  return typeof value === "object" && value !== null && !Array.isArray(value);
59
65
  }
@@ -65,6 +71,160 @@ export function configurationModuleForm(name) {
65
71
  export function isConfigurationDocumentName(name) {
66
72
  return DOCUMENT_NAME.test(name);
67
73
  }
74
+ /** The flags a tool's command line names its configuration file with. */
75
+ const CONFIG_FLAGS = new Set(["--config", "-c"]);
76
+ /** The extensions of a module a configuration flag may name. */
77
+ const MODULE_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"];
78
+ /** The characters that end a word outside quotes and separate nothing: blanks. */
79
+ const BLANK = /[ \t\r\f\v]/u;
80
+ /** The characters that end a word outside quotes and begin an operator: newline included. */
81
+ const OPERATOR = /[\n;&|()<>]/u;
82
+ /** The characters a backslash escapes inside double quotes; before any other it stands for itself. */
83
+ const DOUBLE_QUOTE_ESCAPES = new Set(["$", "`", '"', "\\"]);
84
+ /**
85
+ * The words of one command line, as POSIX token recognition reads quotes, backslashes,
86
+ * line continuations, comments and operators. A word an expansion may change (an unquoted
87
+ * `$`, backquote, glob character or leading `~`, or a `$` or backquote in double quotes)
88
+ * is computed, and so is the rest of a line whose quote is never closed. A computed word
89
+ * and each operator are undefined, so a flag's file is never read across them.
90
+ */
91
+ function shellWords(command) {
92
+ const words = [];
93
+ let word;
94
+ let computed = false;
95
+ const end = () => {
96
+ if (word !== undefined) {
97
+ words.push(computed ? undefined : word);
98
+ }
99
+ word = undefined;
100
+ computed = false;
101
+ };
102
+ for (let at = 0; at < command.length; at += 1) {
103
+ const char = command.charAt(at);
104
+ if (char === "\\" && command.charAt(at + 1) === "\n") {
105
+ at += 1;
106
+ }
107
+ else if (BLANK.test(char)) {
108
+ end();
109
+ }
110
+ else if (OPERATOR.test(char)) {
111
+ end();
112
+ words.push(undefined);
113
+ }
114
+ else if (char === "#" && word === undefined) {
115
+ const newline = command.indexOf("\n", at);
116
+ at = newline < 0 ? command.length : newline - 1;
117
+ }
118
+ else if (char === "\\") {
119
+ at += 1;
120
+ word = (word ?? "") + command.charAt(at);
121
+ }
122
+ else if (char === "'" || char === '"') {
123
+ const close = closingQuote(command, at);
124
+ if (close < 0) {
125
+ word ??= "";
126
+ computed = true;
127
+ break;
128
+ }
129
+ const quoted = char === "'"
130
+ ? { text: command.slice(at + 1, close), computed: false }
131
+ : doubleQuoted(command.slice(at + 1, close));
132
+ word = (word ?? "") + quoted.text;
133
+ computed ||= quoted.computed;
134
+ at = close;
135
+ }
136
+ else {
137
+ computed ||= /[$`*?[]/u.test(char) || (char === "~" && word === undefined);
138
+ word = (word ?? "") + char;
139
+ }
140
+ }
141
+ end();
142
+ return words;
143
+ }
144
+ /** The text between one pair of double quotes, unescaped, and whether an expansion may change it. */
145
+ function doubleQuoted(inner) {
146
+ let text = "";
147
+ let computed = false;
148
+ for (let at = 0; at < inner.length; at += 1) {
149
+ const char = inner.charAt(at);
150
+ const next = inner.charAt(at + 1);
151
+ if (char === "\\" && next === "\n") {
152
+ at += 1;
153
+ }
154
+ else if (char === "\\" && DOUBLE_QUOTE_ESCAPES.has(next)) {
155
+ text += next;
156
+ at += 1;
157
+ }
158
+ else {
159
+ computed ||= char === "$" || char === "`";
160
+ text += char;
161
+ }
162
+ }
163
+ return { text, computed };
164
+ }
165
+ /** The index of the quote that closes the one at `open`, or -1 where none does. */
166
+ function closingQuote(command, open) {
167
+ const quote = command.charAt(open);
168
+ for (let at = open + 1; at < command.length; at += 1) {
169
+ const char = command.charAt(at);
170
+ if (char === quote) {
171
+ return at;
172
+ }
173
+ if (quote === '"' && char === "\\") {
174
+ at += 1;
175
+ }
176
+ }
177
+ return -1;
178
+ }
179
+ /**
180
+ * The files the scripts of the manifest in one directory name after a configuration flag,
181
+ * `--config <file>`, `-c <file>` or `--config=<file>`, each a JSON file or a module below
182
+ * the target root and in no `node_modules`.
183
+ */
184
+ function scriptConfigurations(host, root, dir) {
185
+ let scripts;
186
+ try {
187
+ const manifest = JSON.parse(host.readFile(joinPath(dir, MANIFEST)));
188
+ scripts = isRecord(manifest) ? manifest["scripts"] : undefined;
189
+ }
190
+ catch {
191
+ return [];
192
+ }
193
+ if (!isRecord(scripts)) {
194
+ return [];
195
+ }
196
+ const found = [];
197
+ for (const command of Object.values(scripts)) {
198
+ if (typeof command !== "string") {
199
+ continue;
200
+ }
201
+ const words = shellWords(command);
202
+ words.forEach((word, at) => {
203
+ const named = word !== undefined && CONFIG_FLAGS.has(word)
204
+ ? words[at + 1]
205
+ : word === undefined
206
+ ? undefined
207
+ : /^--config=(.+)$/su.exec(word)?.[1];
208
+ const path = named === undefined ? undefined : resolvePath(dir, named);
209
+ if (path !== undefined &&
210
+ [".json", ...MODULE_EXTENSIONS].some((extension) => path.endsWith(extension)) &&
211
+ isProjectPath(root, path) &&
212
+ host.kindOf(path) === "file") {
213
+ found.push(path);
214
+ }
215
+ });
216
+ }
217
+ return found;
218
+ }
219
+ /** The decoded value of one JSON file, or undefined where it cannot be read or parsed. */
220
+ function readDocument(host, path) {
221
+ try {
222
+ return JSON.parse(host.readFile(path));
223
+ }
224
+ catch {
225
+ return undefined;
226
+ }
227
+ }
68
228
  /** Every string value one decoded JSON value holds at any depth. */
69
229
  export function documentStrings(value) {
70
230
  if (typeof value === "string") {
@@ -116,7 +276,8 @@ export function configurationFiles(host, targetRoot, files, parse) {
116
276
  dirs.add(dir);
117
277
  }
118
278
  }
119
- const documents = [];
279
+ const pending = [];
280
+ const named = [];
120
281
  for (const dir of [...dirs].sort()) {
121
282
  if (!holdsManifest(dir)) {
122
283
  continue;
@@ -130,24 +291,57 @@ export function configurationFiles(host, targetRoot, files, parse) {
130
291
  .sort();
131
292
  }
132
293
  catch {
133
- continue;
294
+ names = [];
134
295
  }
135
- for (const name of names) {
136
- const path = joinPath(dir, name);
137
- let value;
138
- try {
139
- value = JSON.parse(host.readFile(path));
296
+ pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
297
+ for (const path of scriptConfigurations(host, root, dir)) {
298
+ const held = files.byName.get(path);
299
+ if (path.endsWith(".json")) {
300
+ pending.push({ path, manifestDir: dir });
140
301
  }
141
- catch {
302
+ else if (held !== undefined) {
303
+ modules.push({ file: held, form: "a script's configuration flag" });
304
+ }
305
+ else {
306
+ named.push({ path, manifestDir: dir });
307
+ }
308
+ }
309
+ }
310
+ // A document reached from two manifests is read against each, so a chain is keyed by both.
311
+ const documents = new Map();
312
+ const unreadable = new Set();
313
+ for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
314
+ let document = documents.get(next.path);
315
+ if (unreadable.has(next.path) || document?.manifestDirs.includes(next.manifestDir) === true) {
316
+ continue;
317
+ }
318
+ if (document === undefined) {
319
+ const value = readDocument(host, next.path);
320
+ if (value === undefined) {
321
+ unreadable.add(next.path);
142
322
  continue;
143
323
  }
144
- documents.push({ path, strings: documentStrings(value) });
324
+ document = { path: next.path, manifestDirs: [], strings: documentStrings(value) };
325
+ documents.set(next.path, document);
326
+ }
327
+ document.manifestDirs.push(next.manifestDir);
328
+ for (const text of document.strings) {
329
+ const path = RELATIVE_PREFIXES.some((prefix) => text.startsWith(prefix))
330
+ ? resolvePath(dirnamePath(next.path), text)
331
+ : undefined;
332
+ if (path?.endsWith(".json") === true &&
333
+ isProjectPath(root, path) &&
334
+ host.kindOf(path) === "file") {
335
+ pending.push({ path, manifestDir: next.manifestDir });
336
+ }
145
337
  }
146
338
  }
147
339
  return {
148
340
  modules,
149
- documents,
150
- outside: parse === undefined ? [] : outsideModules(host, [...dirs], files, holdsManifest, parse),
341
+ documents: [...documents.values()],
342
+ outside: parse === undefined
343
+ ? []
344
+ : outsideModules(host, root, [...dirs], files, holdsManifest, parse, named),
151
345
  };
152
346
  }
153
347
  /** The relative specifiers one file's imports and re-exports name. */
@@ -192,11 +386,11 @@ export function resolveRelativeSpecifier(host, dir, specifier) {
192
386
  }
193
387
  /**
194
388
  * The configuration modules of the directories that hold a manifest which the program
195
- * does not hold, and the files they import by a relative specifier, until no further
196
- * file joins. A file that cannot be read is skipped.
389
+ * does not hold, and the project files below `root` they import by a relative specifier,
390
+ * until no further file joins. A file that cannot be read is skipped.
197
391
  */
198
- function outsideModules(host, dirs, files, holdsManifest, parse) {
199
- const pending = [];
392
+ function outsideModules(host, root, dirs, files, holdsManifest, parse, named) {
393
+ const pending = [...named];
200
394
  for (const dir of [...dirs].sort()) {
201
395
  if (!holdsManifest(dir)) {
202
396
  continue;
@@ -214,30 +408,36 @@ function outsideModules(host, dirs, files, holdsManifest, parse) {
214
408
  }
215
409
  pending.push(...names.map((name) => ({ path: joinPath(dir, name), manifestDir: dir })));
216
410
  }
217
- const found = [];
218
- const seen = new Set();
411
+ const found = new Map();
412
+ const unreadable = new Set();
219
413
  for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
220
- if (seen.has(next.path) || files.byName.has(next.path)) {
414
+ let outside = found.get(next.path);
415
+ if (unreadable.has(next.path) ||
416
+ files.byName.has(next.path) ||
417
+ outside?.manifestDirs.includes(next.manifestDir) === true) {
221
418
  continue;
222
419
  }
223
- seen.add(next.path);
224
- let text;
225
- try {
226
- text = host.readFile(next.path);
227
- }
228
- catch {
229
- continue;
420
+ if (outside === undefined) {
421
+ let text;
422
+ try {
423
+ text = host.readFile(next.path);
424
+ }
425
+ catch {
426
+ unreadable.add(next.path);
427
+ continue;
428
+ }
429
+ outside = { file: parse(next.path, text), manifestDirs: [] };
430
+ found.set(next.path, outside);
230
431
  }
231
- const file = parse(next.path, text);
232
- found.push({ file, manifestDir: next.manifestDir });
233
- for (const specifier of relativeSpecifiers(file)) {
432
+ outside.manifestDirs.push(next.manifestDir);
433
+ for (const specifier of relativeSpecifiers(outside.file)) {
234
434
  const reached = resolveRelativeSpecifier(host, dirnamePath(next.path), specifier);
235
- if (reached !== undefined) {
435
+ if (reached !== undefined && isProjectPath(root, reached)) {
236
436
  pending.push({ path: reached, manifestDir: next.manifestDir });
237
437
  }
238
438
  }
239
439
  }
240
- return found;
440
+ return [...found.values()];
241
441
  }
242
442
  /** Every dependency the manifest in one directory declares, or none where it holds none to read. */
243
443
  export function declaredIn(host, dir) {
@@ -4,7 +4,7 @@ export const RECORD = {
4
4
  conformance: {
5
5
  corpusVersion: "5.0.0",
6
6
  result: "pass",
7
- digest: "sha256:26c486907df44fb8eaf3f749f60a6010ac94e7cfeeda70c4aa406445193ab80a",
7
+ digest: "sha256:c46c7d06b6f0c58504c5f311870efefc0b85f18c2c46ba7d7b722081cb370c0c",
8
8
  },
9
9
  gaps: [],
10
10
  };
@@ -3,10 +3,10 @@
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
- import { isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isStringLiteral, } from "@typescript/native/unstable/ast";
9
+ import { isClassDeclaration, isClassExpression, isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isMethodSignatureDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, } from "@typescript/native/unstable/ast";
10
10
  import { SignatureKind } from "@typescript/native/unstable/sync";
11
11
  import { isAnswered } from "./query.js";
12
12
  /** The member kinds a contextual type writes and reads: data members and the members of object types. */
@@ -36,6 +36,20 @@ function propertyName(property) {
36
36
  }
37
37
  return undefined;
38
38
  }
39
+ /**
40
+ * Whether an object spread copies a member a declaration declares: a method, a method
41
+ * signature and a class's accessor are no own data property of the value, while an
42
+ * accessor of an interface or a type literal describes one. It is decided on the
43
+ * declaration, because a mapped type over a class keeps the class's declarations on
44
+ * plain property symbols.
45
+ */
46
+ function copiedBySpread(node) {
47
+ if (isMethodDeclaration(node) || isMethodSignatureDeclaration(node)) {
48
+ return false;
49
+ }
50
+ return !((isGetAccessorDeclaration(node) || isSetAccessorDeclaration(node)) &&
51
+ (isClassDeclaration(node.parent) || isClassExpression(node.parent)));
52
+ }
39
53
  /**
40
54
  * The contextual uses of one project's literals and values, asked in batches: one
41
55
  * contextual type per literal and per value whose type carries a member of the target,
@@ -45,6 +59,9 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
45
59
  const queries = project.queries;
46
60
  const ownPaths = project.ownPaths();
47
61
  const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
62
+ const privateNames = new Set(held.symbols
63
+ .filter((symbol) => symbol.visibility === "private-name")
64
+ .map((symbol) => symbol.id));
48
65
  let batches = 0;
49
66
  let lookups = 0;
50
67
  const batchesOf = (count) => Math.ceil(count / cap);
@@ -82,8 +99,12 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
82
99
  ids: chains
83
100
  .declarationsOf(property)
84
101
  .filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
102
+ methods: chains
103
+ .declarationsOf(property)
104
+ .filter((id) => kinds.get(id) === "interface-method"),
85
105
  handles: new Set(property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`)),
86
106
  own: property.declarations.some((handle) => ownPaths.has(handle.path)),
107
+ declarations: property.declarations.filter((handle) => ownPaths.has(handle.path)),
87
108
  });
88
109
  }
89
110
  tables.set(type.id, table);
@@ -133,7 +154,10 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
133
154
  batches += batchesOf(carried.length);
134
155
  const types = queries.typesAt(carried.map((flow) => flow.value), cap);
135
156
  /** The members a position may read structurally: those of object types, not of classes. */
136
- const readable = (member) => member.ids.filter((id) => kinds.get(id) === "type-member");
157
+ const readable = (member) => [
158
+ ...member.ids.filter((id) => kinds.get(id) === "type-member"),
159
+ ...member.methods,
160
+ ];
137
161
  const carries = (part) => [...membersOf(part).values()].some((member) => readable(member).length > 0);
138
162
  /** Whether an object type has members and declares none of them in the target. */
139
163
  const outside = (part) => {
@@ -157,14 +181,36 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
157
181
  elements.set(type.id, element);
158
182
  return element;
159
183
  };
184
+ const argumentsOf = new Map();
185
+ /** The type arguments of an instantiation of a generic type, read once per type. */
186
+ const typeArgumentsOf = (type) => {
187
+ const known = argumentsOf.get(type.id);
188
+ if (known !== undefined) {
189
+ return known;
190
+ }
191
+ lookups += type.isTypeReference() ? 1 : 0;
192
+ const held = queries.typeArguments(type);
193
+ const found = isAnswered(held) ? held : [];
194
+ argumentsOf.set(type.id, found);
195
+ return found;
196
+ };
160
197
  const calls = new Map();
161
- /** The parameter and return types of a type's one call signature, read once per type. */
198
+ /**
199
+ * The parameter and return types of a type's one call signature, or of its one construct
200
+ * signature where it has no call signature, read once per type.
201
+ */
162
202
  const callOf = (type) => {
163
203
  if (calls.has(type.id)) {
164
204
  return calls.get(type.id);
165
205
  }
166
206
  lookups += 1;
167
- const signatures = queries.signaturesOf(type, SignatureKind.Call);
207
+ let kind = SignatureKind.Call;
208
+ let signatures = queries.signaturesOf(type, kind);
209
+ if (isAnswered(signatures) && signatures.length === 0) {
210
+ lookups += 1;
211
+ kind = SignatureKind.Construct;
212
+ signatures = queries.signaturesOf(type, kind);
213
+ }
168
214
  let called;
169
215
  const [signature, ...more] = isAnswered(signatures) ? signatures : [];
170
216
  if (signature !== undefined && more.length === 0) {
@@ -172,6 +218,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
172
218
  const result = queries.returnTypeOf(signature);
173
219
  lookups += parameters.length + 1;
174
220
  called = {
221
+ kind,
175
222
  parameters: parameters.map((one) => (isAnswered(one) ? one : undefined)),
176
223
  result: isAnswered(result) ? result : undefined,
177
224
  };
@@ -181,10 +228,11 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
181
228
  };
182
229
  /**
183
230
  * Each object type a value of `source` is checked as against the object types of
184
- * `target`: the value's own, and through array elements and through a function's
185
- * parameters, checked the other way, and its result. A source that is one of the
186
- * target's own object types matches it as it stands. Each pair is compared once, which
187
- * ends the walk over a recursive type.
231
+ * `target`: the value's own, and through array elements, a function's parameters,
232
+ * checked the other way, and result, and the type arguments two instantiations share
233
+ * by place, an array among them, as an array reaches `Iterable<T>`.
234
+ * A source that is one of the target's own object types matches it as it stands. Each
235
+ * pair is compared once, which ends the walk over a recursive type.
188
236
  */
189
237
  const compared = (source, target, into, seen) => {
190
238
  const key = `${String(source?.id)}>${String(target?.id)}`;
@@ -206,13 +254,24 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
206
254
  }
207
255
  const element = elementOf(one);
208
256
  const called = element === undefined ? callOf(one) : undefined;
257
+ const sourceArguments = typeArgumentsOf(one);
209
258
  for (const part of targets) {
259
+ const partElement = elementOf(part);
260
+ if (element !== undefined && partElement !== undefined) {
261
+ compared(element, partElement, into, seen);
262
+ continue;
263
+ }
264
+ const targetArguments = typeArgumentsOf(part);
265
+ // A defaulted trailing parameter, as `Iterable<T, TReturn, TNext>` has, leaves
266
+ // the places two instantiations share comparable.
267
+ sourceArguments.slice(0, targetArguments.length).forEach((argument, index) => {
268
+ compared(argument, targetArguments[index], into, seen);
269
+ });
210
270
  if (element !== undefined) {
211
- compared(element, elementOf(part), into, seen);
212
271
  continue;
213
272
  }
214
273
  const other = called === undefined ? undefined : callOf(part);
215
- if (called === undefined || other === undefined) {
274
+ if (called === undefined || other?.kind !== called.kind) {
216
275
  continue;
217
276
  }
218
277
  other.parameters.forEach((parameter, index) => {
@@ -239,7 +298,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
239
298
  if (element !== undefined) {
240
299
  return mayCarry(element, seen);
241
300
  }
242
- return callOf(part) !== undefined;
301
+ return callOf(part) !== undefined || typeArgumentsOf(part).some((one) => mayCarry(one, seen));
243
302
  });
244
303
  };
245
304
  const sourced = carried.flatMap((flow, index) => {
@@ -288,5 +347,37 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
288
347
  reads.push({ at: flow.value, from: flow.from, ids: [...ids] });
289
348
  }
290
349
  });
350
+ const copies = new Map();
351
+ /** Whether a spread copies the member: where any declaration of it declares data. */
352
+ const copied = (member) => {
353
+ let known = copies.get(member);
354
+ if (known === undefined) {
355
+ known = member.declarations.some((handle) => {
356
+ const node = project.declarationAt(handle)?.node;
357
+ return node === undefined || copiedBySpread(node);
358
+ });
359
+ copies.set(member, known);
360
+ }
361
+ return known;
362
+ };
363
+ const spreads = literals.flatMap(({ literal, from }) => literal.properties.flatMap((property) => isSpreadAssignment(property) ? [{ value: property.expression, from }] : []));
364
+ batches += batchesOf(spreads.length);
365
+ const spreadTypes = queries.typesAt(spreads.map((one) => one.value), cap);
366
+ spreads.forEach(({ value, from }, index) => {
367
+ const type = spreadTypes[index];
368
+ if (type === undefined || !isAnswered(type)) {
369
+ return;
370
+ }
371
+ // A spread gets every own enumerable property, an overwritten key's getter included,
372
+ // and no private name, which is no property
373
+ // (https://tc39.es/ecma262/#sec-copydataproperties).
374
+ const own = (member, id) => !privateNames.has(id) && copied(member);
375
+ const ids = [
376
+ ...new Set(partsOf(type).flatMap((part) => [...membersOf(part).values()].flatMap((member) => member.ids.filter((id) => own(member, id))))),
377
+ ];
378
+ if (ids.length > 0) {
379
+ reads.push({ at: value, from, ids });
380
+ }
381
+ });
291
382
  return { writes, reads, batches, lookups };
292
383
  }
@@ -39,6 +39,12 @@ const BABEL_CONFIG = [
39
39
  const STRYKER_CONFIG = "{stryker,.stryker}.{conf,config}.{json,js,mjs,cjs}";
40
40
  const STRYKER_MODULE = "{stryker,.stryker}.{conf,config}.{js,mjs,cjs}";
41
41
  const VITEST_CONFIG = `{vitest,vite}.config.${ANY_MODULE}`;
42
+ /**
43
+ * The properties Vitest takes its project root from, `test.root` before Vite's `root`; it
44
+ * resolves `setupFiles` and `globalSetup` against that root
45
+ * (https://vitest.dev/config/setupfiles, https://vitest.dev/config/globalsetup).
46
+ */
47
+ const VITEST_ROOTS = ["test.root", "root"];
42
48
  /** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
43
49
  const ANGULAR_ENTRIES = [
44
50
  "src/{main,main.server,server,polyfills,test}.ts",
@@ -535,5 +541,9 @@ export const CONVENTION_ROWS = [
535
541
  { property: "test.coverage.include", files: [VITEST_CONFIG] },
536
542
  { property: "test.coverage.exclude", files: [VITEST_CONFIG] },
537
543
  ],
544
+ testFiles: [
545
+ { property: "test.setupFiles", files: [VITEST_CONFIG], roots: VITEST_ROOTS },
546
+ { property: "test.globalSetup", files: [VITEST_CONFIG], roots: VITEST_ROOTS },
547
+ ],
538
548
  },
539
549
  ];
@@ -413,6 +413,35 @@ function holdsValue(held, value) {
413
413
  return first !== undefined && literalText(first) === value;
414
414
  }));
415
415
  }
416
+ /** The directory one file's test-file strings are read against, as {@link TestFileReading} states it. */
417
+ function testFileRoot(file, dir, reading) {
418
+ for (const property of reading.roots) {
419
+ const values = valuesAt(file, property, reading.call);
420
+ if (values.length > 0) {
421
+ const texts = new Set(values.map(literalText));
422
+ const [text] = texts;
423
+ return texts.size === 1 && text !== undefined ? resolvePath(dir, text) : undefined;
424
+ }
425
+ }
426
+ return dir;
427
+ }
428
+ /** The files an applied row's test-file properties name. */
429
+ function testFilesNamed(host, parse, dir, readings, into) {
430
+ for (const reading of readings) {
431
+ for (const { file } of readingFiles(host, parse, dir, reading.files)) {
432
+ const at = testFileRoot(file, dir, reading);
433
+ if (at === undefined) {
434
+ continue;
435
+ }
436
+ for (const node of stringsAt(file, reading.property, reading.call)) {
437
+ const text = literalText(node);
438
+ if (text !== undefined && text !== "") {
439
+ into.add(resolvePath(at, text));
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
416
445
  /** The strings an applied row's selection keys hold, added per configuration file to `into`. */
417
446
  function selectedStrings(host, parse, dir, readings, into) {
418
447
  for (const reading of readings) {
@@ -664,6 +693,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
664
693
  const failures = [];
665
694
  const uses = new Set();
666
695
  const selected = new Map();
696
+ const testFiles = new Set();
667
697
  for (const manifest of manifests) {
668
698
  const declared = readJSON(host, joinPath(manifest.dir, "package.json"));
669
699
  const versions = new Map();
@@ -677,7 +707,9 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
677
707
  }
678
708
  const version = versions.get(row.package);
679
709
  if (version === undefined) {
680
- if (missing.has(row.package) || !declares(declared, row.package, REQUIRED_SECTIONS)) {
710
+ if (missing.has(row.package) ||
711
+ manifest.nested === true ||
712
+ !declares(declared, row.package, REQUIRED_SECTIONS)) {
681
713
  continue;
682
714
  }
683
715
  missing.add(row.package);
@@ -735,6 +767,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
735
767
  uses.add(name);
736
768
  }
737
769
  selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
770
+ testFilesNamed(host, parse, manifest.dir, row.testFiles ?? [], testFiles);
738
771
  const excludes = [
739
772
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
740
773
  ].map(globExpression);
@@ -762,6 +795,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
762
795
  failures,
763
796
  uses: [...uses].sort(compare),
764
797
  selected,
798
+ testFiles,
765
799
  };
766
800
  }
767
801
  /**