@cplieger/deadset-ts 7.2.0-dev.3 → 8.0.0-dev.1

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 (153) hide show
  1. package/dist/bin/mapper-connection.js +7 -4
  2. package/dist/src/analysis.js +116 -16
  3. package/dist/src/augmentations.js +21 -8
  4. package/dist/src/automatic-types.js +189 -0
  5. package/dist/src/base-url.js +18 -0
  6. package/dist/src/component-files.js +96 -31
  7. package/dist/src/components.js +7 -6
  8. package/dist/src/config.js +1 -0
  9. package/dist/src/configuration-files.js +54 -15
  10. package/dist/src/configured-declarations.js +24 -2
  11. package/dist/src/conformance-record.js +2 -2
  12. package/dist/src/contextual-uses.js +26 -26
  13. package/dist/src/convention-rows.js +75 -0
  14. package/dist/src/conventions.js +101 -5
  15. package/dist/src/decorator.js +21 -18
  16. package/dist/src/dependencies.js +185 -30
  17. package/dist/src/destructuring.js +77 -2
  18. package/dist/src/discover.js +188 -85
  19. package/dist/src/edges.js +1 -1
  20. package/dist/src/entry-point-gaps.js +7 -10
  21. package/dist/src/entry-points.js +112 -11
  22. package/dist/src/findings/completion.js +116 -21
  23. package/dist/src/findings/dependencies-and-module-machinery.js +71 -25
  24. package/dist/src/findings/emitters.js +1 -1
  25. package/dist/src/findings/intra-function.js +44 -13
  26. package/dist/src/findings/non-code-artifacts.js +66 -33
  27. package/dist/src/findings/unused-declarations.js +21 -1
  28. package/dist/src/findings/visibility-narrowing.js +22 -3
  29. package/dist/src/generated-file.js +11 -0
  30. package/dist/src/git-ignore.js +86 -0
  31. package/dist/src/graph.js +4 -1
  32. package/dist/src/import-bindings.js +97 -0
  33. package/dist/src/indexed-access.js +130 -28
  34. package/dist/src/interface-satisfaction.js +30 -12
  35. package/dist/src/intra-function-parts.js +313 -50
  36. package/dist/src/inventory.js +63 -8
  37. package/dist/src/json-document.js +5 -5
  38. package/dist/src/kinds.js +13 -13
  39. package/dist/src/matrix.js +18 -9
  40. package/dist/src/mdx-entries.js +197 -0
  41. package/dist/src/module-declarations.js +9 -13
  42. package/dist/src/namespace-objects.js +28 -18
  43. package/dist/src/overrides.js +45 -9
  44. package/dist/src/paths.js +9 -0
  45. package/dist/src/print.js +1 -0
  46. package/dist/src/ref.js +10 -0
  47. package/dist/src/references.js +29 -13
  48. package/dist/src/reflective-lookup.js +8 -6
  49. package/dist/src/report.js +4 -2
  50. package/dist/src/reporters/template.js +22 -13
  51. package/dist/src/resolve.js +2 -0
  52. package/dist/src/roots.js +110 -38
  53. package/dist/src/schema.js +1 -0
  54. package/dist/src/serialization-contract.js +97 -20
  55. package/dist/src/session.js +26 -1
  56. package/dist/src/source-files.js +39 -14
  57. package/dist/src/spelled-names.js +58 -0
  58. package/dist/src/suppress-file.js +12 -4
  59. package/dist/src/suppress-inline.js +21 -3
  60. package/dist/src/suppress.js +7 -5
  61. package/dist/src/sweep.js +27 -10
  62. package/dist/src/symbol-keys.js +103 -2
  63. package/dist/src/template-field.js +9 -6
  64. package/dist/src/test-support.js +121 -17
  65. package/dist/src/type-errors.js +116 -35
  66. package/dist/src/type-parameter-reads.js +172 -0
  67. package/dist/src/value-reach.js +17 -5
  68. package/dist/src/verbs/analyze.js +19 -1
  69. package/dist/src/verbs/explain.js +4 -2
  70. package/dist/src/version-range.js +10 -5
  71. package/dist/src/version.js +1 -1
  72. package/dist/src/workflow-steps.js +119 -22
  73. package/dist/src/workspace-programs.js +7 -3
  74. package/dist/src/workspace-resolution.js +72 -13
  75. package/dist/src/workspace.js +1 -1
  76. package/jsr.json +1 -1
  77. package/package.json +2 -2
  78. package/src/analysis.ts +175 -16
  79. package/src/augmentations.ts +21 -10
  80. package/src/automatic-types.ts +246 -0
  81. package/src/base-url.ts +24 -0
  82. package/src/component-files.ts +116 -31
  83. package/src/components.ts +7 -6
  84. package/src/config.ts +3 -0
  85. package/src/configuration-files.ts +80 -18
  86. package/src/configured-declarations.ts +33 -4
  87. package/src/conformance-record.ts +2 -2
  88. package/src/contextual-uses.ts +27 -27
  89. package/src/convention-rows.ts +103 -0
  90. package/src/conventions.ts +121 -7
  91. package/src/decorator.ts +28 -18
  92. package/src/dependencies.ts +249 -43
  93. package/src/destructuring.ts +93 -2
  94. package/src/discover.ts +234 -86
  95. package/src/edges.ts +1 -1
  96. package/src/entry-point-gaps.ts +8 -11
  97. package/src/entry-points.ts +133 -11
  98. package/src/finding.ts +10 -0
  99. package/src/findings/completion.ts +147 -24
  100. package/src/findings/dependencies-and-module-machinery.ts +90 -28
  101. package/src/findings/emitters.ts +1 -1
  102. package/src/findings/intra-function.ts +56 -14
  103. package/src/findings/non-code-artifacts.ts +81 -41
  104. package/src/findings/unused-declarations.ts +25 -1
  105. package/src/findings/visibility-narrowing.ts +32 -3
  106. package/src/generated-file.ts +16 -0
  107. package/src/git-ignore.ts +98 -0
  108. package/src/graph.ts +6 -0
  109. package/src/host.ts +5 -9
  110. package/src/import-bindings.ts +128 -0
  111. package/src/indexed-access.ts +152 -30
  112. package/src/interface-satisfaction.ts +33 -13
  113. package/src/intra-function-parts.ts +415 -64
  114. package/src/inventory.ts +83 -7
  115. package/src/json-document.ts +7 -7
  116. package/src/kinds.ts +13 -13
  117. package/src/matrix.ts +32 -14
  118. package/src/mdx-entries.ts +241 -0
  119. package/src/module-declarations.ts +11 -13
  120. package/src/namespace-objects.ts +34 -21
  121. package/src/overrides.ts +56 -9
  122. package/src/paths.ts +10 -0
  123. package/src/print.ts +1 -0
  124. package/src/ref.ts +12 -0
  125. package/src/references.ts +38 -17
  126. package/src/reflective-lookup.ts +9 -7
  127. package/src/report.ts +16 -3
  128. package/src/reporters/template.ts +21 -13
  129. package/src/resolve.ts +3 -0
  130. package/src/roots.ts +163 -45
  131. package/src/schema.ts +1 -0
  132. package/src/serialization-contract.ts +108 -17
  133. package/src/session.ts +29 -1
  134. package/src/source-files.ts +66 -16
  135. package/src/spelled-names.ts +102 -0
  136. package/src/suppress-file.ts +14 -3
  137. package/src/suppress-inline.ts +21 -2
  138. package/src/suppress.ts +9 -5
  139. package/src/sweep.ts +32 -11
  140. package/src/symbol-keys.ts +129 -2
  141. package/src/template-field.ts +9 -6
  142. package/src/test-support.ts +149 -16
  143. package/src/type-errors.ts +132 -41
  144. package/src/type-parameter-reads.ts +227 -0
  145. package/src/value-reach.ts +19 -6
  146. package/src/verbs/analyze.ts +28 -1
  147. package/src/verbs/explain.ts +5 -2
  148. package/src/version-range.ts +10 -5
  149. package/src/version.ts +1 -1
  150. package/src/workflow-steps.ts +127 -22
  151. package/src/workspace-programs.ts +7 -3
  152. package/src/workspace-resolution.ts +86 -14
  153. package/src/workspace.ts +1 -1
package/src/sweep.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { TSExemptionClass } from "./exempt-classes.ts";
10
10
  import { namesACaller, OUTSIDE, type Edge, type Graph } from "./graph.ts";
11
+ import type { InventorySymbol } from "./inventory.ts";
11
12
  import type { Position } from "./position.ts";
12
13
 
13
14
  /** The liveness relation that decided a declaration, as a finding spells it. */
@@ -178,17 +179,32 @@ export function sweep(graph: Graph, input: SweepInput): Liveness {
178
179
  return { liveUnder: live, candidates };
179
180
  }
180
181
 
182
+ /**
183
+ * The directory a test runner reads a manual mock from, whose exports it puts in place of
184
+ * the module's: https://jestjs.io/docs/manual-mocks
185
+ */
186
+ const MANUAL_MOCKS = "__mocks__";
187
+
188
+ /** Whether a test runner calls or reads one export of a test file: a function, or any export of a manual mock. */
189
+ export function runnerCalls(symbol: InventorySymbol): boolean {
190
+ return symbol.kind === "function" || symbol.position.path.split("/").includes(MANUAL_MOCKS);
191
+ }
192
+
181
193
  /**
182
194
  * Per declaration of the graph, whether the run that counts test references reaches it.
183
- * That run's roots are the graph's own and each test file, being the file's top level
184
- * and every declaration it exports, which a test runner may call; `held` seeds it as a
185
- * mark or an exemption seeds a sweep.
195
+ * That run's roots are the graph's own and, of each test file, its top level and every
196
+ * function it exports, which a test runner may call, and every export of a manual mock,
197
+ * which the runner reads; any other export of a test file, and test-support code, which
198
+ * runs where a test imports it, are judged as any file's are.
199
+ * `held` seeds it as a mark or an exemption seeds a sweep.
186
200
  */
187
201
  export function reachedWithTheTests(graph: Graph, held: readonly string[]): readonly boolean[] {
188
202
  const tests = graph.symbols.flatMap((symbol, at) => {
189
- const top =
190
- symbol.kind === "file" || graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
191
- return graph.test[at] === true && top && (symbol.kind === "file" || symbol.exported)
203
+ const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
204
+ const called = exported && runnerCalls(symbol);
205
+ return graph.test[at] === true &&
206
+ graph.support[at] !== true &&
207
+ (symbol.kind === "file" || called)
192
208
  ? [{ at, kind: "test-runner" as const }]
193
209
  : [];
194
210
  });
@@ -277,10 +293,11 @@ function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly
277
293
 
278
294
  /**
279
295
  * Admits every test declaration whose set of referenced production declarations is
280
- * non-empty and wholly dead, and returns which it admitted. The set is read from every
281
- * reference the test makes, which a production sweep does not count. A test-support
282
- * declaration that a test referencing a live target also references is a live target of
283
- * every test referencing it. A marked or exempt test is never admitted.
296
+ * non-empty and wholly dead, and returns which it admitted. An ambient declaration describes
297
+ * code the target does not implement, so it is no production declaration here. The set is
298
+ * read from every reference the test makes, which a production sweep does not count. A
299
+ * test-support declaration a test referencing a live target also references is a live
300
+ * target of every test referencing it. A marked or exempt test is never admitted.
284
301
  */
285
302
  function admitTestsOfDeadCode(
286
303
  graph: Graph,
@@ -293,7 +310,11 @@ function admitTestsOfDeadCode(
293
310
  let live = 0;
294
311
  if (testCode(at) && graph.subject[at] === true && held[at] !== true) {
295
312
  for (const edge of graph.out[at] ?? []) {
296
- if (testCode(edge.to) || graph.subject[edge.to] !== true) {
313
+ if (
314
+ testCode(edge.to) ||
315
+ graph.subject[edge.to] !== true ||
316
+ graph.ambient[edge.to] === true
317
+ ) {
297
318
  continue;
298
319
  }
299
320
  targets += 1;
@@ -2,24 +2,31 @@
2
2
  * The members a unique symbol keys, used through element accesses. `value[key]`, where
3
3
  * `key` is a constant of a `unique symbol` type, reads the member of the receiver's type
4
4
  * whose computed name is that same constant, and a store through the access writes it.
5
- * No name node resolves to such a member: the key's name resolves to the constant.
5
+ * No name node resolves to such a member: the key's name resolves to the constant. An
6
+ * iteration reads the member its well-known iteration symbol keys.
6
7
  */
7
8
 
8
9
  import {
9
10
  isComputedPropertyName,
11
+ isForOfStatement,
10
12
  isIdentifier,
11
13
  isNoSubstitutionTemplateLiteral,
12
14
  isNumericLiteral,
13
15
  isParenthesizedExpression,
14
16
  isPropertyAccessExpression,
17
+ isSpreadElement,
15
18
  isStringLiteral,
16
19
  type ElementAccessExpression,
20
+ type Expression,
17
21
  type Node,
22
+ type SourceFile,
18
23
  } from "@typescript/native/ast";
19
24
  import type { Symbol as TSSymbol, Type } from "@typescript/native/sync";
20
25
  import type { AliasChains } from "./alias-chain.ts";
21
- import type { InventorySymbol } from "./inventory.ts";
26
+ import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
27
+ import { renderPosition } from "./position.ts";
22
28
  import { isAnswered, type PropertyTables } from "./query.ts";
29
+ import type { Reference } from "./references.ts";
23
30
  import type { ProjectView } from "./session.ts";
24
31
 
25
32
  /** One element access whose key is a name, and how the access uses the member it selects. */
@@ -170,3 +177,123 @@ export function keyedUses<Brand>(
170
177
  return ids.length === 0 ? [] : [{ access: one.access, ids: [...new Set(ids)] }];
171
178
  });
172
179
  }
180
+
181
+ /** The well-known symbol a value's iteration calls, by the construct that iterates it. */
182
+ type Protocol = "iterator" | "asyncIterator";
183
+
184
+ /** The member name `[Symbol.<name>]` spells, where a computed name spells one. */
185
+ function wellKnownName(name: Node | undefined): string | undefined {
186
+ if (name === undefined || !isComputedPropertyName(name)) {
187
+ return undefined;
188
+ }
189
+ const key = name.expression;
190
+ return isPropertyAccessExpression(key) &&
191
+ isIdentifier(key.expression) &&
192
+ key.expression.text === "Symbol" &&
193
+ isIdentifier(key.name)
194
+ ? key.name.text
195
+ : undefined;
196
+ }
197
+
198
+ /**
199
+ * The references the iterations of one project's own files make to the iteration members
200
+ * of the values they iterate: a `for…of` statement and a spread element read
201
+ * `[Symbol.iterator]`, and a `for await…of` statement reads `[Symbol.asyncIterator]` and,
202
+ * which it falls back to, `[Symbol.iterator]`. Each is made by the declaration holding it.
203
+ */
204
+ export function iterationReferences<Brand>(
205
+ project: ProjectView<Brand>,
206
+ held: Inventory,
207
+ chains: AliasChains,
208
+ root: string,
209
+ testFiles: ReadonlySet<string>,
210
+ ): readonly Reference[] {
211
+ const queries = project.queries;
212
+ const ownPaths = project.ownPaths();
213
+ const iterated: {
214
+ readonly file: SourceFile;
215
+ readonly from: string;
216
+ readonly operand: Expression;
217
+ readonly protocols: readonly Protocol[];
218
+ }[] = [];
219
+ for (const file of project.ownSourceFiles()) {
220
+ const fileId = held.declarations.get(nodeKey(file, file));
221
+ if (fileId === undefined) {
222
+ continue;
223
+ }
224
+ const visit = (node: Node, from: string): void => {
225
+ const holder = held.declarations.get(nodeKey(file, node)) ?? from;
226
+ if (isForOfStatement(node)) {
227
+ const protocols: readonly Protocol[] =
228
+ node.awaitModifier === undefined ? ["iterator"] : ["asyncIterator", "iterator"];
229
+ iterated.push({ file, from: holder, operand: node.expression, protocols });
230
+ } else if (isSpreadElement(node)) {
231
+ iterated.push({ file, from: holder, operand: node.expression, protocols: ["iterator"] });
232
+ }
233
+ node.forEachChild((child) => {
234
+ visit(child, holder);
235
+ });
236
+ };
237
+ file.forEachChild((child) => {
238
+ visit(child, fileId);
239
+ });
240
+ }
241
+ if (iterated.length === 0) {
242
+ return [];
243
+ }
244
+ const types = queries.typesAt(iterated.map((one) => one.operand));
245
+ const members = new Map<
246
+ number,
247
+ readonly { readonly name: string; readonly ids: readonly string[] }[]
248
+ >();
249
+ const membersOf = (
250
+ type: Type,
251
+ ): readonly { readonly name: string; readonly ids: readonly string[] }[] => {
252
+ const known = members.get(type.id);
253
+ if (known !== undefined) {
254
+ return known;
255
+ }
256
+ const keyed: { name: string; ids: readonly string[] }[] = [];
257
+ const properties = queries.propertiesOf(type);
258
+ for (const property of isAnswered(properties) ? properties : []) {
259
+ for (const handle of property.declarations) {
260
+ const node = ownPaths.has(handle.path) ? project.declarationAt(handle)?.node : undefined;
261
+ const name = wellKnownName((node as { readonly name?: Node } | undefined)?.name);
262
+ if (name !== undefined) {
263
+ keyed.push({ name, ids: chains.declarationsOf(property) });
264
+ break;
265
+ }
266
+ }
267
+ }
268
+ members.set(type.id, keyed);
269
+ return keyed;
270
+ };
271
+ const found: Reference[] = [];
272
+ iterated.forEach((one, index) => {
273
+ const type = types[index];
274
+ if (type === undefined || !isAnswered(type)) {
275
+ return;
276
+ }
277
+ const parts =
278
+ type.isUnionType() || type.isIntersectionType() ? queries.constituents(type) : [type];
279
+ const position = renderPosition(one.file, root, one.operand.getStart());
280
+ for (const part of isAnswered(parts) ? parts : []) {
281
+ for (const member of membersOf(part)) {
282
+ if (!(one.protocols as readonly string[]).includes(member.name)) {
283
+ continue;
284
+ }
285
+ for (const to of member.ids) {
286
+ found.push({
287
+ from: one.from,
288
+ to,
289
+ position,
290
+ use: "read",
291
+ resolution: "keyed",
292
+ test: testFiles.has(position.path),
293
+ });
294
+ }
295
+ }
296
+ }
297
+ });
298
+ return found;
299
+ }
@@ -18,7 +18,7 @@ import type { DetectorInput, Evidence } from "./exempt.ts";
18
18
  import type { Host } from "./host.ts";
19
19
  import type { SymbolKind } from "./inventory.ts";
20
20
  import { memberNames } from "./member-names.ts";
21
- import { isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.ts";
21
+ import { extensionOf, isAbsolutePath, joinPath, normalizePath, relativePath } from "./paths.ts";
22
22
  import { isComponentFile, type Position } from "./position.ts";
23
23
 
24
24
  /** One template, by its path below the target root. */
@@ -125,7 +125,10 @@ function stringEnd(text: string, from: number): number {
125
125
  return at + 1;
126
126
  }
127
127
 
128
- /** The end of the `${` substitution whose expression starts at `from`, past its closing brace. */
128
+ /**
129
+ * The end of the `${` substitution whose expression starts at `from`: past its closing brace,
130
+ * or one past the text's end where none closes it, so the expression is the text to its end.
131
+ */
129
132
  function substitutionEnd(text: string, from: number): number {
130
133
  let depth = 1;
131
134
  let at = from;
@@ -149,7 +152,7 @@ function substitutionEnd(text: string, from: number): number {
149
152
  }
150
153
  at += 1;
151
154
  }
152
- return at;
155
+ return text.length + 1;
153
156
  }
154
157
 
155
158
  /** The end of the template literal at `from`; each `${…}` expression goes to `substitution`. */
@@ -243,8 +246,8 @@ function namesOf(text: string, delimiters: TemplateDelimiters): readonly Named[]
243
246
  }
244
247
 
245
248
  /** Every identifier the markup of one component file writes in an action. */
246
- function markupNamesOf(text: string): readonly Named[] {
247
- return readComponent(text).markup.flatMap((one) =>
249
+ function markupNamesOf(file: TemplateFile): readonly Named[] {
250
+ return readComponent(file.text, extensionOf(file.path)).markup.flatMap((one) =>
248
251
  identifiersOf(one.text, true).map(({ name, offset }) => ({
249
252
  name,
250
253
  offset: one.start + offset,
@@ -287,7 +290,7 @@ export function templateField<Brand>(input: DetectorInput<Brand>): readonly Evid
287
290
  names: namesOf(file.text, templates.delimiters),
288
291
  markup: false,
289
292
  })),
290
- ...components.map((file) => ({ file, names: markupNamesOf(file.text), markup: true })),
293
+ ...components.map((file) => ({ file, names: markupNamesOf(file), markup: true })),
291
294
  ];
292
295
  for (const { file, names, markup } of scanned) {
293
296
  for (const named of names) {
@@ -1,10 +1,13 @@
1
1
  /**
2
- * Test-support code: the files only tests reach. A test reaches each one through a chain
3
- * of references, and every reference to a declaration of one, an import for its effects
4
- * included, is written in a test file or in another such file, so it is judged with the
5
- * tests: what it references is referenced by test code.
2
+ * Test-support code: the files only tests reach that carry test evidence of their own. A
3
+ * test reaches each one through a chain of references, and every reference to a
4
+ * declaration of one, an import for its effects included, is written in a test file or in
5
+ * another such file, so it is judged with the tests: what it references is referenced by
6
+ * test code.
6
7
  */
7
8
 
9
+ import { CONVENTION_ROWS } from "./convention-rows.ts";
10
+ import { packageOfSpecifier } from "./dependencies.ts";
8
11
  import type { InventorySymbol } from "./inventory.ts";
9
12
  import type { Reference } from "./references.ts";
10
13
  import type { Root } from "./roots.ts";
@@ -12,18 +15,139 @@ import type { Root } from "./roots.ts";
12
15
  /** The root kind that never takes a file out of test-support code: the skip only withholds. */
13
16
  const SKIP_ROOT = "type-error";
14
17
 
18
+ /**
19
+ * The test runners and test libraries an import of which is test evidence: each runner a
20
+ * convention row reads test files for, the platform's own runner, and the libraries below.
21
+ */
22
+ const TEST_PACKAGES: ReadonlySet<string> = new Set([
23
+ ...CONVENTION_ROWS.filter((row) => row.testFiles !== undefined).map((row) => row.package),
24
+ "node:test",
25
+ "@playwright/test",
26
+ ]);
27
+
28
+ /** The scopes every package of which is a test library. */
29
+ const TEST_SCOPES = ["@testing-library/"] as const;
30
+
31
+ /**
32
+ * The Node.js built-in modules a bare specifier names, which are no package. The set is
33
+ * closed: Node.js adds a new built-in under the `node:` scheme alone
34
+ * (https://nodejs.org/api/modules.html#built-in-modules-with-mandatory-node-prefix).
35
+ */
36
+ const NODE_BUILTINS: ReadonlySet<string> = new Set([
37
+ "assert",
38
+ "async_hooks",
39
+ "buffer",
40
+ "child_process",
41
+ "cluster",
42
+ "console",
43
+ "constants",
44
+ "crypto",
45
+ "dgram",
46
+ "diagnostics_channel",
47
+ "dns",
48
+ "domain",
49
+ "events",
50
+ "fs",
51
+ "http",
52
+ "http2",
53
+ "https",
54
+ "inspector",
55
+ "module",
56
+ "net",
57
+ "os",
58
+ "path",
59
+ "perf_hooks",
60
+ "process",
61
+ "punycode",
62
+ "querystring",
63
+ "readline",
64
+ "repl",
65
+ "stream",
66
+ "string_decoder",
67
+ "sys",
68
+ "timers",
69
+ "tls",
70
+ "trace_events",
71
+ "tty",
72
+ "url",
73
+ "util",
74
+ "v8",
75
+ "vm",
76
+ "wasi",
77
+ "worker_threads",
78
+ "zlib",
79
+ ]);
80
+
81
+ /** What a file imports, read for test evidence. */
82
+ export interface TestEvidence {
83
+ /** Per file below the target root, the module specifiers its imports write. */
84
+ readonly imports: ReadonlyMap<string, readonly string[]>;
85
+ /**
86
+ * Whether the manifest nearest the file, below the target root, that declares the
87
+ * package declares it under `devDependencies`.
88
+ */
89
+ readonly devDependency: (path: string, name: string) => boolean;
90
+ }
91
+
92
+ /** Whether a specifier names a test runner or a test library. */
93
+ function namesTestPackage(specifier: string): boolean {
94
+ const name = specifier.startsWith("node:") ? specifier : packageOfSpecifier(specifier);
95
+ return (
96
+ name !== undefined &&
97
+ (TEST_PACKAGES.has(name) || TEST_SCOPES.some((scope) => name.startsWith(scope)))
98
+ );
99
+ }
100
+
101
+ /**
102
+ * Whether a file carries test evidence: an import of a test runner or a test library, or
103
+ * package imports, Node built-ins aside, that are all devDependencies no file imports
104
+ * outside the test files and `onlyTests`, the files only tests reach.
105
+ */
106
+ function evidenceReader(
107
+ tests: ReadonlySet<string>,
108
+ evidence: TestEvidence,
109
+ ): (onlyTests: ReadonlySet<string>, path: string) => boolean {
110
+ const importers = new Map<string, string[]>();
111
+ for (const [path, specifiers] of evidence.imports) {
112
+ for (const specifier of specifiers) {
113
+ const name = packageOfSpecifier(specifier);
114
+ if (name !== undefined) {
115
+ importers.set(name, [...(importers.get(name) ?? []), path]);
116
+ }
117
+ }
118
+ }
119
+ return (onlyTests, path) => {
120
+ const specifiers = evidence.imports.get(path) ?? [];
121
+ if (specifiers.some(namesTestPackage)) {
122
+ return true;
123
+ }
124
+ const names = specifiers.flatMap((one) => packageOfSpecifier(one) ?? []);
125
+ const packages = names.filter((name) => !NODE_BUILTINS.has(name));
126
+ return (
127
+ packages.length > 0 &&
128
+ packages.every(
129
+ (name) =>
130
+ evidence.devDependency(path, name) &&
131
+ (importers.get(name) ?? []).every((one) => tests.has(one) || onlyTests.has(one)),
132
+ )
133
+ );
134
+ };
135
+ }
136
+
15
137
  /**
16
138
  * The files of one project that are test-support code, below the target root: the
17
139
  * largest set of files a test file reaches, none of them a test file or holding a root,
18
- * whose every user outside the set is a test file. A root has a caller outside the
19
- * reference graph: a runtime, a tool, a configuration, or a consumer of a library's
20
- * published API. `testFiles` are the files the test-file rules classified.
140
+ * each carrying test evidence of its own, whose every user outside the set is a test
141
+ * file. A root has a caller outside the reference graph: a runtime, a tool, a
142
+ * configuration, or a consumer of a library's published API. `testFiles` are the files
143
+ * the test-file rules classified.
21
144
  */
22
145
  export function testSupportFiles(
23
146
  symbols: readonly InventorySymbol[],
24
147
  references: readonly Reference[],
25
148
  roots: readonly Root[],
26
149
  testFiles: readonly string[],
150
+ evidence: TestEvidence,
27
151
  ): ReadonlySet<string> {
28
152
  const fileOf = new Map(symbols.map((symbol) => [symbol.id, symbol.position.path]));
29
153
  const users = new Map<string, Set<string>>();
@@ -59,17 +183,26 @@ export function testSupportFiles(
59
183
  }
60
184
  }
61
185
  }
62
- const support = new Set([...reached].filter((path) => !rooted.has(path)));
63
- for (let changed = true; changed;) {
64
- changed = false;
65
- for (const path of support) {
66
- const from = users.get(path) ?? new Set<string>();
67
- if (![...from].every((one) => tests.has(one) || support.has(one))) {
68
- support.delete(path);
69
- changed = true;
186
+ const withoutOutsideUsers = (files: Set<string>): Set<string> => {
187
+ for (let changed = true; changed;) {
188
+ changed = false;
189
+ for (const path of files) {
190
+ const from = users.get(path) ?? new Set<string>();
191
+ if (![...from].every((one) => tests.has(one) || files.has(one))) {
192
+ files.delete(path);
193
+ changed = true;
194
+ }
70
195
  }
71
196
  }
72
- }
197
+ return files;
198
+ };
199
+ // Evidence reads the files only tests reach, evidence or none, so it is fixed before the
200
+ // files without evidence leave the set.
201
+ const onlyTests = withoutOutsideUsers(new Set([...reached].filter((path) => !rooted.has(path))));
202
+ const carries = evidenceReader(tests, evidence);
203
+ const support = withoutOutsideUsers(
204
+ new Set([...onlyTests].filter((path) => carries(onlyTests, path))),
205
+ );
73
206
  return support;
74
207
  }
75
208