@cplieger/deadset-ts 7.0.0-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 (77) hide show
  1. package/dist/src/analysis.js +6 -16
  2. package/dist/src/component-layer.js +1 -1
  3. package/dist/src/configuration-files.js +227 -27
  4. package/dist/src/contextual-uses.js +41 -7
  5. package/dist/src/convention-rows.js +6 -0
  6. package/dist/src/conventions.js +32 -0
  7. package/dist/src/dependencies.js +12 -8
  8. package/dist/src/discover.js +1 -1
  9. package/dist/src/edges.js +1 -1
  10. package/dist/src/exempt-classes.js +9 -22
  11. package/dist/src/exempt.js +3 -15
  12. package/dist/src/findings/completion.js +35 -4
  13. package/dist/src/findings/emitters.js +0 -4
  14. package/dist/src/findings/self-check.js +14 -10
  15. package/dist/src/indexed-access.js +8 -2
  16. package/dist/src/inventory.js +3 -12
  17. package/dist/src/references.js +2 -2
  18. package/dist/src/report.js +3 -0
  19. package/dist/src/roots.js +0 -1
  20. package/dist/src/run.js +0 -2
  21. package/dist/src/schema.js +1 -1
  22. package/dist/src/scope.js +2 -2
  23. package/dist/src/session.js +0 -1
  24. package/dist/src/suppress-inline.js +1 -1
  25. package/dist/src/suppress.js +15 -16
  26. package/dist/src/workspace-programs.js +0 -3
  27. package/dist/src/workspace-resolution.js +0 -2
  28. package/dist/src/workspace.js +0 -3
  29. package/jsr.json +1 -1
  30. package/package.json +2 -1
  31. package/src/analysis.ts +10 -30
  32. package/src/calls.ts +3 -3
  33. package/src/component-files.ts +7 -7
  34. package/src/component-layer.ts +2 -2
  35. package/src/components.ts +1 -1
  36. package/src/config.ts +4 -4
  37. package/src/configuration-files.ts +246 -28
  38. package/src/contextual-uses.ts +63 -9
  39. package/src/convention-rows.ts +19 -0
  40. package/src/conventions.ts +43 -0
  41. package/src/dependencies.ts +14 -10
  42. package/src/discover.ts +3 -3
  43. package/src/edges.ts +3 -3
  44. package/src/entry-points.ts +1 -1
  45. package/src/exempt-classes.ts +10 -24
  46. package/src/exempt.ts +5 -29
  47. package/src/file-facts.ts +1 -1
  48. package/src/finding.ts +3 -1
  49. package/src/findings/completion.ts +46 -4
  50. package/src/findings/emitters.ts +3 -8
  51. package/src/findings/self-check.ts +17 -14
  52. package/src/findings-pass.ts +2 -2
  53. package/src/graph.ts +2 -2
  54. package/src/html-entries.ts +1 -1
  55. package/src/indexed-access.ts +10 -2
  56. package/src/inventory.ts +5 -33
  57. package/src/json-document.ts +1 -1
  58. package/src/manifest.ts +1 -1
  59. package/src/namespace-objects.ts +1 -1
  60. package/src/pnpm-workspace.ts +1 -1
  61. package/src/references.ts +7 -6
  62. package/src/report.ts +8 -4
  63. package/src/roots.ts +0 -3
  64. package/src/run.ts +0 -2
  65. package/src/schema.ts +2 -2
  66. package/src/scope.ts +2 -2
  67. package/src/session.ts +0 -2
  68. package/src/stores.ts +1 -1
  69. package/src/suppress-inline.ts +3 -3
  70. package/src/suppress.ts +21 -24
  71. package/src/template-field.ts +1 -1
  72. package/src/value-reach.ts +1 -1
  73. package/src/verbs/verb.ts +1 -4
  74. package/src/workspace-programs.ts +2 -9
  75. package/src/workspace-resolution.ts +0 -3
  76. package/src/workspace-run.ts +1 -1
  77. package/src/workspace.ts +1 -9
@@ -23,7 +23,7 @@ import { evidenceRecords, heldRecords, disabledClasses, exemptionsOf, holdingWhi
23
23
  import { heldByInclusion, sourcePaths, sourceTree } from "./file-facts.js";
24
24
  import { globExpression } from "./glob.js";
25
25
  import { findingsPass, recordedFindings, rowKey } from "./findings-pass.js";
26
- import { NO_SELF_CHECK, unmatchedRootFindings, } from "./findings/self-check.js";
26
+ import { unmatchedRootFindings } from "./findings/self-check.js";
27
27
  import { frameworkLifecycle } from "./framework-lifecycle.js";
28
28
  import { implementations, mergeImplementations } from "./implementations.js";
29
29
  import { injectionContainer } from "./injection-container.js";
@@ -243,6 +243,7 @@ function readProjects(engine, host, scope, config, stage) {
243
243
  .filter((file) => referenceOnly(file.fileName))
244
244
  .flatMap((file) => relativePath(absoluteRoot, file.fileName) ?? []),
245
245
  generated: generatedFilesOf(own, generatedFiles(conventions, own)),
246
+ testFileNames: conventions.testFiles,
246
247
  };
247
248
  };
248
249
  for (const file of project.ownSourceFiles()) {
@@ -346,7 +347,7 @@ export function runRoots(engine, host, scope, config, provenance) {
346
347
  };
347
348
  }
348
349
  /** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
349
- const DETECTORS = new Map([
350
+ export const DETECTORS = new Map([
350
351
  ["interface-satisfaction", interfaceSatisfaction],
351
352
  ["enum-group", enumGroup],
352
353
  ["generated-file", generatedFile],
@@ -374,6 +375,7 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
374
375
  const projectRead = opened.read();
375
376
  const resolved = references(project, projectRead.held, targetRoot, {
376
377
  testFiles: config.ts.testFiles,
378
+ testFileNames: projectRead.testFileNames,
377
379
  });
378
380
  const evidence = evidenceRecords({
379
381
  project,
@@ -419,7 +421,6 @@ function readRun(engine, host, scope, config, mode, detectors, extra) {
419
421
  const { projectRead } = one;
420
422
  const satisfied = settled[index];
421
423
  const detected = heldRecords(satisfied?.evidence ?? one.evidence, {
422
- disabled,
423
424
  mode,
424
425
  testFiles: new Set([...one.testFiles, ...support]),
425
426
  });
@@ -623,7 +624,7 @@ export function runSweep(engine, host, scope, config, input, detectors = DETECTO
623
624
  return sweptOf(readRun(engine, host, scope, config, input.mode, detectors, () => undefined), input);
624
625
  }
625
626
  /** Reads a run's projects with every per-project fact an emitter reads. */
626
- function readEmitterRun(engine, host, scope, config, mode, detectors) {
627
+ export function readEmitterRun(engine, host, scope, config, mode, detectors) {
627
628
  const targetRoot = scope.target.path;
628
629
  const run = readRun(engine, host, scope, config, mode, detectors, (project, read, references) => ({
629
630
  deprecation: deprecatedDeclarations(project, read.held),
@@ -682,7 +683,7 @@ function nearestManifestDir(host, root, file) {
682
683
  * The input every emitter reads over one sweep of a read run: the facts beside the sweep
683
684
  * each kind family reads, every per-project one read in the same session as the sweep.
684
685
  */
685
- function emitterInputOver(host, scope, config, read, input, selfCheck) {
686
+ export function emitterInputOver(host, scope, config, read, input, selfCheck) {
686
687
  const targetRoot = scope.target.path;
687
688
  const swept = sweptOf(read, input);
688
689
  const { projects } = read;
@@ -714,17 +715,6 @@ function emitterInputOver(host, scope, config, read, input, selfCheck) {
714
715
  intraFunction: intraFunctionFacts(projects.map((one) => one.extra.parts), projects.flatMap((one) => one.configured.references), new Set([...swept.exempt.map((one) => one.id), ...swept.heldByUnanswered])),
715
716
  };
716
717
  }
717
- /**
718
- * What every emitter reads of the run the scope and the configuration describe, swept
719
- * under the caller's marks: the run's sweep, the facts beside it each kind family reads,
720
- * and the sides of the target's declared cross-language edges that name a symbol of this
721
- * language. The run reads no suppression document, so the self-check family has nothing
722
- * to report.
723
- */
724
- export function runEmitterInput(engine, host, scope, config, input, detectors = DETECTORS) {
725
- const read = readEmitterRun(engine, host, scope, config, input.mode, detectors);
726
- return emitterInputOver(host, scope, config, read, input, NO_SELF_CHECK);
727
- }
728
718
  /**
729
719
  * Reads the run the scope and the configuration describe for its findings. The inline
730
720
  * directives, the ignore file's entries and the baseline's rows are bound before any
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { dirnamePath, joinPath } from "./paths.js";
9
9
  /** The name the mapper's manifest declares and every configuration's entry names. */
10
- export const MAPPER_PACKAGE = "deadset-ts-component-files";
10
+ const MAPPER_PACKAGE = "deadset-ts-component-files";
11
11
  /** The manifest of the mapper's package, which runs `command`. */
12
12
  export function mapperManifest(command) {
13
13
  return `${JSON.stringify({
@@ -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, [...dirs], files, holdsManifest, parse, named),
151
345
  };
152
346
  }
153
347
  /** The relative specifiers one file's imports and re-exports name. */
@@ -195,8 +389,8 @@ export function resolveRelativeSpecifier(host, dir, specifier) {
195
389
  * does not hold, and the files they import by a relative specifier, until no further
196
390
  * 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, 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
435
  if (reached !== undefined) {
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) {
@@ -3,11 +3,11 @@
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";
10
- import { SignatureKind } from "@typescript/native/unstable/sync";
9
+ import { isComputedPropertyName, isGetAccessorDeclaration, isIdentifier, isMethodDeclaration, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isPropertyAssignment, isSetAccessorDeclaration, isShorthandPropertyAssignment, isSpreadAssignment, isStringLiteral, } from "@typescript/native/unstable/ast";
10
+ import { SignatureKind, SymbolFlags } 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. */
13
13
  const MEMBER_KINDS = new Set(["type-member", "class-member"]);
@@ -45,6 +45,9 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
45
45
  const queries = project.queries;
46
46
  const ownPaths = project.ownPaths();
47
47
  const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
48
+ const privateNames = new Set(held.symbols
49
+ .filter((symbol) => symbol.visibility === "private-name")
50
+ .map((symbol) => symbol.id));
48
51
  let batches = 0;
49
52
  let lookups = 0;
50
53
  const batchesOf = (count) => Math.ceil(count / cap);
@@ -84,6 +87,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
84
87
  .filter((id) => MEMBER_KINDS.has(kinds.get(id) ?? "file")),
85
88
  handles: new Set(property.declarations.map((handle) => `${handle.path}#${String(handle.index)}`)),
86
89
  own: property.declarations.some((handle) => ownPaths.has(handle.path)),
90
+ accessor: (property.flags & SymbolFlags.Accessor) !== 0,
87
91
  });
88
92
  }
89
93
  tables.set(type.id, table);
@@ -158,13 +162,22 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
158
162
  return element;
159
163
  };
160
164
  const calls = new Map();
161
- /** The parameter and return types of a type's one call signature, read once per type. */
165
+ /**
166
+ * The parameter and return types of a type's one call signature, or of its one construct
167
+ * signature where it has no call signature, read once per type.
168
+ */
162
169
  const callOf = (type) => {
163
170
  if (calls.has(type.id)) {
164
171
  return calls.get(type.id);
165
172
  }
166
173
  lookups += 1;
167
- const signatures = queries.signaturesOf(type, SignatureKind.Call);
174
+ let kind = SignatureKind.Call;
175
+ let signatures = queries.signaturesOf(type, kind);
176
+ if (isAnswered(signatures) && signatures.length === 0) {
177
+ lookups += 1;
178
+ kind = SignatureKind.Construct;
179
+ signatures = queries.signaturesOf(type, kind);
180
+ }
168
181
  let called;
169
182
  const [signature, ...more] = isAnswered(signatures) ? signatures : [];
170
183
  if (signature !== undefined && more.length === 0) {
@@ -172,6 +185,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
172
185
  const result = queries.returnTypeOf(signature);
173
186
  lookups += parameters.length + 1;
174
187
  called = {
188
+ kind,
175
189
  parameters: parameters.map((one) => (isAnswered(one) ? one : undefined)),
176
190
  result: isAnswered(result) ? result : undefined,
177
191
  };
@@ -212,7 +226,7 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
212
226
  continue;
213
227
  }
214
228
  const other = called === undefined ? undefined : callOf(part);
215
- if (called === undefined || other === undefined) {
229
+ if (called === undefined || other?.kind !== called.kind) {
216
230
  continue;
217
231
  }
218
232
  other.parameters.forEach((parameter, index) => {
@@ -288,5 +302,25 @@ export function contextualUses(project, held, chains, literals, flows, cap, prop
288
302
  reads.push({ at: flow.value, from: flow.from, ids: [...ids] });
289
303
  }
290
304
  });
305
+ const spreads = literals.flatMap(({ literal, from }) => literal.properties.flatMap((property) => isSpreadAssignment(property) ? [{ value: property.expression, from }] : []));
306
+ batches += batchesOf(spreads.length);
307
+ const spreadTypes = queries.typesAt(spreads.map((one) => one.value), cap);
308
+ spreads.forEach(({ value, from }, index) => {
309
+ const type = spreadTypes[index];
310
+ if (type === undefined || !isAnswered(type)) {
311
+ return;
312
+ }
313
+ // A spread gets every own enumerable property, an overwritten key's getter included.
314
+ // So it reads each data member but a class's accessor, which sits on the prototype with
315
+ // the class's methods (never data members), and a private name, which is no property
316
+ // (https://tc39.es/ecma262/#sec-copydataproperties).
317
+ const own = (member, id) => !privateNames.has(id) && !(member.accessor && kinds.get(id) === "class-member");
318
+ const ids = [
319
+ ...new Set(partsOf(type).flatMap((part) => [...membersOf(part).values()].flatMap((member) => member.ids.filter((id) => own(member, id))))),
320
+ ];
321
+ if (ids.length > 0) {
322
+ reads.push({ at: value, from, ids });
323
+ }
324
+ });
291
325
  return { writes, reads, batches, lookups };
292
326
  }
@@ -39,6 +39,11 @@ 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` against that root (https://vitest.dev/config/setupfiles).
45
+ */
46
+ const VITEST_ROOTS = ["test.root", "root"];
42
47
  /** The entry files of an Angular CLI project, at the workspace root or below `projects`. */
43
48
  const ANGULAR_ENTRIES = [
44
49
  "src/{main,main.server,server,polyfills,test}.ts",
@@ -535,5 +540,6 @@ export const CONVENTION_ROWS = [
535
540
  { property: "test.coverage.include", files: [VITEST_CONFIG] },
536
541
  { property: "test.coverage.exclude", files: [VITEST_CONFIG] },
537
542
  ],
543
+ testFiles: [{ property: "test.setupFiles", files: [VITEST_CONFIG], roots: VITEST_ROOTS }],
538
544
  },
539
545
  ];
@@ -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();
@@ -735,6 +765,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
735
765
  uses.add(name);
736
766
  }
737
767
  selectedStrings(host, parse, manifest.dir, row.selections ?? [], selected);
768
+ testFilesNamed(host, parse, manifest.dir, row.testFiles ?? [], testFiles);
738
769
  const excludes = [
739
770
  ...new Set((row.excludes ?? []).flatMap((one) => expand(one, resolved))),
740
771
  ].map(globExpression);
@@ -762,6 +793,7 @@ export function readConventions(host, parse, manifests, disabled, rows = CONVENT
762
793
  failures,
763
794
  uses: [...uses].sort(compare),
764
795
  selected,
796
+ testFiles,
765
797
  };
766
798
  }
767
799
  /**
@@ -244,16 +244,20 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
244
244
  use(ownersOf(file, held, node), dependenciesNamed(text, names));
245
245
  }
246
246
  }
247
- for (const document of configuration.documents) {
248
- const names = declaredAt(document.path);
249
- for (const text of document.strings) {
250
- use([PROJECT_OWNER], dependenciesNamed(text, names));
247
+ for (const { strings, manifestDirs } of configuration.documents) {
248
+ for (const manifestDir of manifestDirs) {
249
+ const names = declaredAt(joinPath(manifestDir, "package.json"));
250
+ for (const text of strings) {
251
+ use([PROJECT_OWNER], dependenciesNamed(text, names));
252
+ }
251
253
  }
252
254
  }
253
- for (const { file, manifestDir } of configuration.outside) {
254
- const names = declaredAt(joinPath(manifestDir, "package.json"));
255
- for (const { text } of moduleStrings(file)) {
256
- use([PROJECT_OWNER], dependenciesNamed(text, names));
255
+ for (const { file, manifestDirs } of configuration.outside) {
256
+ for (const manifestDir of manifestDirs) {
257
+ const names = declaredAt(joinPath(manifestDir, "package.json"));
258
+ for (const { text } of moduleStrings(file)) {
259
+ use([PROJECT_OWNER], dependenciesNamed(text, names));
260
+ }
257
261
  }
258
262
  }
259
263
  return { packages, uses, unanswered };
@@ -23,7 +23,7 @@ export class DiscoveryError extends Error {
23
23
  /** The category an error diagnostic carries, which the protocol sends as a number. */
24
24
  const ERROR_CATEGORY = DiagnosticCategory.Error;
25
25
  /** Whether one diagnostic is an error, which is what fails a run closed. */
26
- export function isError(diagnostic) {
26
+ function isError(diagnostic) {
27
27
  return diagnostic.category === ERROR_CATEGORY;
28
28
  }
29
29
  /**
package/dist/src/edges.js CHANGED
@@ -25,7 +25,7 @@ const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and a
25
25
  const ID_WANT = "one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
26
26
  const REFERENCE_WANT = "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
27
27
  /** An edges document the Contract refuses: a malformed input, answered with the usage code. */
28
- export class EdgesError extends ConfigError {
28
+ class EdgesError extends ConfigError {
29
29
  constructor(at, text, want) {
30
30
  super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
31
31
  this.name = "EdgesError";