@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.19

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 (86) hide show
  1. package/dist/src/analysis.js +30 -72
  2. package/dist/src/augmentations.js +1 -1
  3. package/dist/src/calls.js +267 -48
  4. package/dist/src/commands/held.js +499 -0
  5. package/dist/src/commands/read.js +16 -10
  6. package/dist/src/commands/shell.js +109 -15
  7. package/dist/src/commands/tools.js +1540 -149
  8. package/dist/src/conformance-record.js +1 -1
  9. package/dist/src/contract/contract.generated.js +9 -2
  10. package/dist/src/entries/conventions.js +7 -6
  11. package/dist/src/entries/test-runners.js +6 -40
  12. package/dist/src/entries/workflow-steps.js +202 -28
  13. package/dist/src/exempt.js +0 -26
  14. package/dist/src/exposure.js +400 -0
  15. package/dist/src/findings/completion.js +4 -17
  16. package/dist/src/findings/emitters.js +22 -33
  17. package/dist/src/findings/intra-function.js +3 -3
  18. package/dist/src/findings/non-code-artifacts.js +11 -5
  19. package/dist/src/findings/reads-and-writes.js +71 -15
  20. package/dist/src/findings/unused-declarations.js +2 -22
  21. package/dist/src/findings/visibility-narrowing.js +14 -20
  22. package/dist/src/inventory.js +18 -0
  23. package/dist/src/{components.js → liveness/components.js} +1 -1
  24. package/dist/src/{graph.js → liveness/graph.js} +38 -21
  25. package/dist/src/liveness/matrix.js +99 -0
  26. package/dist/src/liveness/sweep.js +558 -0
  27. package/dist/src/query.js +46 -0
  28. package/dist/src/references/sink.js +87 -1
  29. package/dist/src/references.js +1 -1
  30. package/dist/src/roots/manifest.js +47 -4
  31. package/dist/src/roots/published-reach.js +4 -4
  32. package/dist/src/roots/root-set.js +14 -16
  33. package/dist/src/text/named-files.js +257 -34
  34. package/dist/src/verbs/analyze.js +16 -1
  35. package/dist/src/verbs/explain.js +73 -152
  36. package/dist/src/verbs/print-retained.js +1 -1
  37. package/jsr.json +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis.ts +54 -108
  40. package/src/augmentations.ts +1 -1
  41. package/src/calls.ts +326 -52
  42. package/src/commands/held.ts +632 -0
  43. package/src/commands/read.ts +26 -12
  44. package/src/commands/shell.ts +117 -15
  45. package/src/commands/tools.ts +1972 -202
  46. package/src/conformance-record.ts +1 -1
  47. package/src/contract/contract.generated.ts +9 -2
  48. package/src/dependencies.ts +1 -1
  49. package/src/entries/conventions.ts +8 -12
  50. package/src/entries/test-runners.ts +12 -45
  51. package/src/entries/workflow-steps.ts +227 -38
  52. package/src/exempt.ts +31 -46
  53. package/src/exposure.ts +492 -0
  54. package/src/file-facts.ts +5 -0
  55. package/src/finding.ts +1 -1
  56. package/src/findings/boundary.ts +7 -0
  57. package/src/findings/completion.ts +5 -19
  58. package/src/findings/emitters.ts +23 -41
  59. package/src/findings/interfaces.ts +3 -3
  60. package/src/findings/intra-function.ts +3 -3
  61. package/src/findings/non-code-artifacts.ts +16 -5
  62. package/src/findings/reads-and-writes.ts +99 -26
  63. package/src/findings/unused-declarations.ts +4 -28
  64. package/src/findings/visibility-narrowing.ts +25 -26
  65. package/src/interface-satisfaction.ts +1 -1
  66. package/src/inventory.ts +19 -0
  67. package/src/{components.ts → liveness/components.ts} +3 -3
  68. package/src/{graph.ts → liveness/graph.ts} +74 -26
  69. package/src/liveness/matrix.ts +162 -0
  70. package/src/liveness/sweep.ts +855 -0
  71. package/src/query.ts +81 -0
  72. package/src/references/sink.ts +101 -0
  73. package/src/references.ts +8 -1
  74. package/src/roots/manifest.ts +53 -8
  75. package/src/roots/published-reach.ts +9 -4
  76. package/src/roots/root-set.ts +18 -16
  77. package/src/stores.ts +2 -1
  78. package/src/tests/file-class.ts +19 -2
  79. package/src/text/named-files.ts +333 -36
  80. package/src/verbs/analyze.ts +16 -1
  81. package/src/verbs/explain.ts +81 -199
  82. package/src/verbs/print-retained.ts +1 -1
  83. package/dist/src/matrix.js +0 -275
  84. package/dist/src/sweep.js +0 -234
  85. package/src/matrix.ts +0 -466
  86. package/src/sweep.ts +0 -384
@@ -1,12 +1,11 @@
1
1
  import { EXIT_CLEAN, EXIT_USAGE } from "../contract/contract.generated.js";
2
2
  import { productionMode, runAnalysis } from "../analysis.js";
3
- import { listing } from "../components.js";
4
- import { OUTSIDE, swept as judged } from "../graph.js";
5
- import { referenceKey } from "../matrix.js";
3
+ import { listing } from "../liveness/components.js";
4
+ import { swept as judged } from "../liveness/graph.js";
6
5
  import { partialNotes } from "../partial.js";
7
6
  import { resolvePath } from "../paths.js";
8
7
  import { positionKey } from "../position.js";
9
- import { madeByTestCode, withheldBy } from "../tests/file-class.js";
8
+ import { madeByTestCode } from "../tests/file-class.js";
10
9
  import { resolve } from "../resolve.js";
11
10
  import {} from "./verb.js";
12
11
  import { UsageError } from "../usage.js";
@@ -80,172 +79,94 @@ function subjectOf(symbols, named) {
80
79
  .sort(bytewise);
81
80
  return { candidates: partial.slice(0, MAX_PARTIAL_MATCHES) };
82
81
  }
83
- /** Every reference of the run once, a reference several configurations make counted once. */
84
- function referencesOf(configured) {
85
- const seen = new Set();
86
- const held = [];
87
- for (const reference of configured.flatMap((one) => one.references)) {
88
- const key = referenceKey(reference);
89
- if (!seen.has(key)) {
90
- seen.add(key);
91
- held.push(reference);
92
- }
93
- }
94
- return held;
95
- }
96
- /** Every root of the run once per reason, in the order the projects name them. */
97
- function rootsOf(configured) {
98
- const seen = new Set();
99
- return configured
100
- .flatMap((one) => one.roots)
101
- .filter((root) => {
102
- const key = [root.id, root.kind, root.source].join("\u0000");
103
- if (seen.has(key)) {
104
- return false;
105
- }
106
- seen.add(key);
107
- return true;
108
- });
109
- }
110
- /**
111
- * The shortest path from a root, a loaded consumer or a test file no project holds to one
112
- * declaration, walked breadth first from every root at once in the order the run names them,
113
- * a reference of a consumer or of such a file being the first hop of a path of its own, as
114
- * the sweep seeds them, and following the references the sweep follows: a
115
- * reference that reads a module reaches what the module exports, and an evaluation or a root
116
- * reaches the module alone. Under `production` no reference a test file made is followed,
117
- * which is the set the report's sweep counts.
118
- */
119
- function shortestPath(analysis, to, mode, support) {
120
- const graph = analysis.swept.matrix.union;
121
- const out = new Map();
122
- const consumed = [];
123
- for (const reference of referencesOf(analysis.configured)) {
124
- if (withheldBy(mode, reference, support)) {
125
- continue;
126
- }
127
- const { consumer } = reference;
128
- if (consumer !== undefined) {
129
- consumed.push({ reference, origin: { k: "consumer", consumer } });
130
- continue;
131
- }
132
- if (reference.unheld === true) {
133
- consumed.push({
134
- reference,
135
- origin: { k: "unheld", file: reference.from.replace(/#$/u, "") },
136
- });
137
- continue;
138
- }
139
- const held = out.get(reference.from);
140
- if (held === undefined) {
141
- out.set(reference.from, [reference]);
142
- }
143
- else {
144
- held.push(reference);
145
- }
146
- }
147
- const steps = [];
148
- const seen = new Set();
149
- const expanded = new Set();
150
- const enter = (step) => {
151
- if (!seen.has(step.at)) {
152
- seen.add(step.at);
153
- steps.push(step);
154
- }
155
- };
156
- const follow = (reference, previous, origin) => {
157
- enter({ at: reference.to, hop: { k: "reference", reference }, previous, origin });
158
- const target = graph.at(reference.to);
159
- if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
160
- return;
161
- }
162
- expanded.add(reference.to);
163
- for (const exported of graph.exportsOf[target] ?? []) {
164
- const id = graph.symbols[exported]?.id;
165
- if (id !== undefined) {
166
- enter({ at: id, hop: { k: "export", reference, to: id }, previous, origin });
167
- }
168
- }
169
- };
170
- for (const root of rootsOf(analysis.configured)) {
171
- enter({ at: root.id, hop: undefined, previous: -1, origin: { k: "root", root } });
172
- }
173
- // A consumed reference is one hop long, so it joins the walk where the roots' first hops do.
174
- const firstHops = steps.length;
175
- for (let head = 0;; head += 1) {
176
- if (head === firstHops) {
177
- for (const { reference, origin } of consumed) {
178
- follow(reference, -1, origin);
179
- }
180
- }
181
- const step = steps[head];
182
- if (step === undefined) {
183
- break;
184
- }
185
- if (step.at === to) {
186
- const path = [];
187
- for (let at = step; at?.hop !== undefined; at = steps[at.previous]) {
188
- path.unshift(at.hop);
189
- }
190
- return { origin: step.origin, path };
191
- }
192
- for (const reference of out.get(step.at) ?? []) {
193
- follow(reference, head, step.origin);
194
- }
195
- }
196
- return undefined;
197
- }
198
82
  /** Where a path starts, as a line names it. */
199
- function originText(origin) {
83
+ function originText(origin, graph) {
84
+ const id = (at) => graph.symbols[at]?.id ?? "";
200
85
  switch (origin.k) {
201
86
  case "root":
202
- return `root ${origin.root.kind} ${origin.root.id}`;
203
- case "consumer":
204
- return `consumer ${origin.consumer}`;
205
- case "unheld":
206
- return `test file no project holds ${origin.file}`;
87
+ return `root ${origin.rooted.kind} ${id(origin.rooted.at)}`;
88
+ case "consumed": {
89
+ const reference = graph.references[origin.edge.reference];
90
+ if (reference?.consumer !== undefined) {
91
+ return `consumer ${reference.consumer}`;
92
+ }
93
+ return reference?.unheld === true
94
+ ? `test file no project holds ${reference.from.replace(/#$/u, "")}`
95
+ : `declaration no configuration judges ${reference?.from ?? ""}`;
96
+ }
97
+ case "exemption":
98
+ return `exemption ${origin.record.class} ${positionKey(origin.record.site)}`;
99
+ case "mark":
100
+ return `suppression of ${id(origin.at)}`;
101
+ case "unanswered":
102
+ return `unanswered question about ${id(origin.at)}`;
207
103
  }
208
104
  }
209
105
  /** What made a reference: a declaration of the target, or a loaded consumer. */
210
106
  function madeBy(reference) {
211
107
  return reference.consumer === undefined ? reference.from : `consumer ${reference.consumer}`;
212
108
  }
213
- /** One reference of an answer, marked where test code made it, a test-support file's included. */
214
- function referenceLine(reference, tail, support) {
215
- return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${madeByTestCode(reference, support) ? "\tfrom a test file" : ""}\n`;
109
+ /**
110
+ * One reference of an answer, marked where test code made it, a test-support file's included,
111
+ * and naming the configurations that make it where they are not every one of the run.
112
+ */
113
+ function referenceLine(reference, tail, support, configurations) {
114
+ const partial = configurations.making.length < configurations.run.length;
115
+ return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${madeByTestCode(reference, support) ? "\tfrom a test file" : ""}${partial ? `\tmade in ${configurations.making.join(" ")}` : ""}\n`;
216
116
  }
217
117
  /** Why a declaration no finding names and no exemption held back is live, or how it fell. */
218
- function liveness(analysis, subject, cascade, mode) {
219
- const { sweep } = analysis.swept;
118
+ function liveness(analysis, subject, cascade) {
119
+ const { matrix, sweep } = analysis.swept;
120
+ const graph = matrix.union;
220
121
  const candidate = sweep.candidates.find((one) => one.id === subject.id);
221
122
  if (candidate === undefined && !judged(subject.kind)) {
222
123
  return `answer: unjudged\n unjudged: neither liveness relation judges a ${subject.kind}\n`;
223
124
  }
224
125
  let text = `answer: ${candidate === undefined ? "live" : "dead"}\n`;
225
126
  text += ` live under: ${listed(sweep.liveUnder.get(subject.id) ?? [])}\n`;
226
- for (const root of rootsOf(analysis.configured).filter((one) => one.id === subject.id)) {
227
- text += ` root: ${root.kind}${root.source === "" ? "" : `\tnamed by ${root.source}`}\n`;
127
+ const at = graph.at(subject.id);
128
+ const seen = new Set();
129
+ for (const root of graph.rooted.filter((one) => one.at === at)) {
130
+ const key = `${root.kind}\u0000${root.source}`;
131
+ if (!seen.has(key)) {
132
+ seen.add(key);
133
+ text += ` root: ${root.kind}${root.source === "" ? "" : `\tnamed by ${root.source}`}\n`;
134
+ }
228
135
  }
229
136
  const support = new Set(analysis.configured.flatMap((one) => one.supportFiles ?? []));
230
- const found = shortestPath(analysis, subject.id, mode, support) ??
231
- shortestPath(analysis, subject.id, { production: false }, support);
232
- if (found !== undefined && found.path.length > 0) {
233
- const { origin } = found;
234
- text += ` reached from: ${originText(origin)}\n`;
235
- for (const hop of found.path) {
236
- text += referenceLine(hop.reference, `${madeBy(hop.reference)} -> ${hop.reference.to}`, support);
137
+ const line = (edge, tail) => {
138
+ const reference = graph.references[edge.reference];
139
+ return reference === undefined
140
+ ? ""
141
+ : referenceLine(reference, tail(reference), support, {
142
+ making: graph.configurations[edge.reference] ?? [],
143
+ run: matrix.configurations,
144
+ });
145
+ };
146
+ const found = sweep.path(subject.id);
147
+ if (found !== undefined && found.hops.length > 0) {
148
+ text += ` reached from: ${originText(found.origin, graph)}\n`;
149
+ for (const hop of found.hops) {
150
+ text += line(hop.edge, (reference) => `${madeBy(reference)} -> ${reference.to}`);
237
151
  if (hop.k === "export") {
238
- text += ` export: ${hop.reference.to} -> ${hop.to}\n`;
152
+ const reference = graph.references[hop.edge.reference];
153
+ text += ` export: ${reference?.to ?? ""} -> ${graph.symbols[hop.to]?.id ?? ""}\n`;
239
154
  }
240
155
  }
241
156
  }
242
157
  else if (found === undefined) {
243
- const references = referencesOf(analysis.configured).filter((one) => one.to === subject.id);
158
+ const references = graph.references.flatMap((one, place) => one.to === subject.id ? [place] : []);
244
159
  text +=
245
160
  " unreachable: no path of references reaches it from a root, a loaded consumer or a test file no project holds\n";
246
161
  text += ` references: ${String(references.length)}\n`;
247
- for (const reference of references) {
248
- text += referenceLine(reference, `from ${madeBy(reference)}`, support);
162
+ for (const place of references) {
163
+ const reference = graph.references[place];
164
+ if (reference !== undefined) {
165
+ text += referenceLine(reference, `from ${madeBy(reference)}`, support, {
166
+ making: graph.configurations[place] ?? [],
167
+ run: matrix.configurations,
168
+ });
169
+ }
249
170
  }
250
171
  }
251
172
  if (candidate !== undefined) {
@@ -294,7 +215,7 @@ function namedElsewhere(findings, subject) {
294
215
  .join("");
295
216
  }
296
217
  /** The one explanation that applies to a declaration, built whole before it is written. */
297
- function explanation(analysis, subject, cascade, mode) {
218
+ function explanation(analysis, subject, cascade) {
298
219
  const graph = analysis.swept.matrix.union;
299
220
  const at = graph.at(subject.id);
300
221
  let text = `symbol: ${subject.ref}\n`;
@@ -302,7 +223,7 @@ function explanation(analysis, subject, cascade, mode) {
302
223
  text += `position: ${subject.id}\n`;
303
224
  text += `configurations: ${listed(analysis.swept.matrix.heldIn[at] ?? [])}\n`;
304
225
  const found = analysis.result.findings.find((one) => positionKey(one.position) === subject.id);
305
- const retained = analysis.swept.retained.filter((one) => one.id === subject.id);
226
+ const retained = analysis.swept.sweep.retained.filter((one) => one.id === subject.id);
306
227
  if (found !== undefined) {
307
228
  text += reported(found);
308
229
  }
@@ -312,22 +233,22 @@ function explanation(analysis, subject, cascade, mode) {
312
233
  text += ` class: ${one.class}\t${positionKey(one.site)}\t${one.detail}\n`;
313
234
  }
314
235
  }
315
- else if (analysis.swept.heldByUnanswered.includes(subject.id)) {
236
+ else if (analysis.swept.sweep.held.includes(subject.id)) {
316
237
  text +=
317
238
  "answer: held\n held by: a question the checker did not answer, which could have kept it live, so no finding names it\n";
318
239
  }
319
240
  else {
320
- text += liveness(analysis, subject, cascade, mode);
241
+ text += liveness(analysis, subject, cascade);
321
242
  }
322
243
  return text + namedElsewhere(analysis.result.findings, subject);
323
244
  }
324
245
  /**
325
246
  * Explains one symbol from the analysis the report is built from: why a reported symbol is
326
247
  * reported, which exemption classes held a retained one back, that an unanswered question held
327
- * a held one, and for any other declaration the relations that hold it live and the shortest
328
- * path from a root, a loaded consumer or a test file no project holds, or the references it
329
- * has where no path reaches it. A request naming no one declaration exits with the usage
330
- * code and names what partially matches it. Where the analysis was partial, the error stream says so.
248
+ * a held one, and for any other declaration the relations that hold it live and the path the
249
+ * sweep's strongest tier reaching it follows, or the references it has where no path reaches
250
+ * it. A request naming no one declaration exits with the usage code and names what partially
251
+ * matches it. Where the analysis was partial, the error stream says so.
331
252
  */
332
253
  export const explainVerb = ({ out, err, host, inputs, scope, openClient, option }) => {
333
254
  const named = namedSymbol(option);
@@ -351,7 +272,7 @@ export const explainVerb = ({ out, err, host, inputs, scope, openClient, option
351
272
  }
352
273
  }
353
274
  else {
354
- out.write(explanation(analysis, subject, config.reporters.cascade, mode));
275
+ out.write(explanation(analysis, subject, config.reporters.cascade));
355
276
  }
356
277
  for (const note of notes) {
357
278
  diagnose(err, note);
@@ -20,7 +20,7 @@ export const printRetainedVerb = ({ out, err, host, inputs, scope, openClient })
20
20
  marked: [],
21
21
  mode: productionMode(config),
22
22
  });
23
- for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
23
+ for (const line of retainedLines(swept.matrix.union.symbols, swept.sweep.retained)) {
24
24
  out.write(`${line}\n`);
25
25
  }
26
26
  for (const note of partialNotes({
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.17",
3
+ "version": "8.0.0-dev.19",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "8.0.0-dev.17",
3
+ "version": "8.0.0-dev.19",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
package/src/analysis.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  } from "./roots/configured-declarations.ts";
18
18
  import { decorator } from "./decorator.ts";
19
19
  import { configurationNeeds, dependenciesOf, projectNeeds } from "./dependencies.ts";
20
+ import { carriersOf, NO_CARRIERS, type Carriers } from "./exposure.ts";
20
21
  import { deprecatedDeclarations, type Deprecation } from "./deprecation.ts";
21
22
  import { moduleIdentity, type LoadedConsumer, type NotBuilt } from "./load/discover.ts";
22
23
  import type { LibraryUnfound } from "./load/classify.ts";
@@ -35,9 +36,7 @@ import {
35
36
  heldRecords,
36
37
  disabledClasses,
37
38
  exemptionsOf,
38
- holdingWhile,
39
39
  isUnansweredRecord,
40
- retainedIn,
41
40
  writtenRecords,
42
41
  type Detector,
43
42
  type Detectors,
@@ -57,15 +56,7 @@ import { intraFunctionFacts, projectParts, type ProjectParts } from "./intra-fun
57
56
  import { type Inventory, type InventorySymbol } from "./inventory.ts";
58
57
  import { inventory } from "./inventory/enumerate.ts";
59
58
  import { readManifest, type ManifestEntry } from "./roots/manifest.ts";
60
- import {
61
- liveFilesAt,
62
- matrixOf,
63
- referenceKey,
64
- sweepMatrix,
65
- type Configured,
66
- type Matrix,
67
- type SweepResult,
68
- } from "./matrix.ts";
59
+ import { matrixOf, referenceKey, type Configured, type Matrix } from "./liveness/matrix.ts";
69
60
  import {
70
61
  ancestors,
71
62
  dirnamePath,
@@ -108,7 +99,9 @@ import { accessorsOf, storesOf } from "./stores.ts";
108
99
  import { isPartKind, type SuppressionRecord, type Suppressions, type Verdict } from "./suppress.ts";
109
100
  import { bindRows, readBaseline, readIgnoreFile, type Recorded } from "./suppress-file.ts";
110
101
  import { inlineDirectives, inlineSuppressions, type InlineDirective } from "./suppress-inline.ts";
111
- import type { Exemption, Mode, SweepInput } from "./sweep.ts";
102
+ import { sweep, type Sweep, type SweepPolicy } from "./liveness/sweep.ts";
103
+ import type { Exemption } from "./exempt.ts";
104
+ import type { Mode } from "./tests/file-class.ts";
112
105
  import { readTemplates, templateField } from "./template-field.ts";
113
106
  import { bytewise } from "./text/compare.ts";
114
107
  import { holdsManifest, manifestAt } from "./fs/manifest.ts";
@@ -434,27 +427,13 @@ export function productionMode(config: PartialConfig): Mode {
434
427
  /** The run's sweep, beside the matrix it judged. */
435
428
  export interface RunSweep {
436
429
  readonly matrix: Matrix;
437
- readonly sweep: SweepResult;
438
- /** The exemption records that hold under the sweep. */
439
- readonly exempt: readonly Exemption[];
440
- /**
441
- * The exemption records that held back a declaration some configuration of the run
442
- * would otherwise judge dead, in the declarations' site order.
443
- */
444
- readonly retained: readonly Exemption[];
430
+ readonly sweep: Sweep;
445
431
  /** The derived configurations the run dropped. */
446
432
  readonly notBuilt: readonly NotBuilt[];
447
433
  /** Every question the checker could not answer, each once. */
448
434
  readonly unanswered: readonly UnansweredQuestion[];
449
- /**
450
- * Every declaration an unanswered question could have kept live, and every one only
451
- * such a declaration keeps live. The run reports none of them.
452
- */
453
- readonly heldByUnanswered: readonly string[];
454
435
  /** The lines of the units type errors skipped, on which the run reports nothing. */
455
436
  readonly skipped: readonly SkippedUnit[];
456
- /** The component files the run read, by path below the target root, each once. */
457
- readonly componentFiles: readonly string[];
458
437
  }
459
438
 
460
439
  /** One `<script` start tag of a component file the run read no block for. */
@@ -527,6 +506,8 @@ interface ReadRun<Extra> {
527
506
  readonly projects: readonly SweptProject<Extra>[];
528
507
  /** Every declared consumer, each loaded, in the scope's order. */
529
508
  readonly consumers: readonly LoadedConsumer[];
509
+ /** Whether the scope declares a consumer, loaded or not. */
510
+ readonly declaresConsumers: boolean;
530
511
  /** The derived configurations the run dropped. */
531
512
  readonly notBuilt: readonly NotBuilt[];
532
513
  /** Every question the checker could not answer, each once. */
@@ -826,6 +807,7 @@ function readRun<Extra>(
826
807
  ],
827
808
  projects,
828
809
  consumers: read.plan.discovery.consumers,
810
+ declaresConsumers: scope.consumers.length > 0,
829
811
  notBuilt: read.classified.notBuilt,
830
812
  unanswered: read.unanswered,
831
813
  componentsUnread: read.plan.workspace?.componentsUnread ?? [],
@@ -840,33 +822,6 @@ function readRun<Extra>(
840
822
  };
841
823
  }
842
824
 
843
- /**
844
- * The run swept under one set of marks with the records that hold. A record a component
845
- * file's markup holds counts only while the file is live, and what it holds may be what
846
- * keeps the file live, so each file's markup joins only once a sweep without it finds the
847
- * file live, until a sweep finds no further file live.
848
- */
849
- function holdingSweep<Extra>(
850
- read: ReadRun<Extra>,
851
- input: Pick<SweepInput, "marked" | "mode">,
852
- ): { readonly swept: SweepInput; readonly result: SweepResult } {
853
- const paths = new Set(read.exempt.flatMap((record) => record.whileLive ?? []));
854
- let live = new Set<string>();
855
- for (;;) {
856
- const swept: SweepInput = {
857
- ...input,
858
- exempt: holdingWhile(read.exempt, live),
859
- unanswered: read.unansweredHeld,
860
- };
861
- const result = sweepMatrix(read.matrix, swept);
862
- const next = new Set(liveFilesAt(read.matrix, result, paths).map((one) => one.position.path));
863
- if (next.size === live.size) {
864
- return { swept, result };
865
- }
866
- live = next;
867
- }
868
- }
869
-
870
825
  /**
871
826
  * The test references exemption evidence written in a test file makes: each is a read of
872
827
  * the declaration it would retain, made by the test file at the evidence's site.
@@ -939,62 +894,27 @@ function writtenOnly<Extra>(read: ReadRun<Extra>, mode: Mode): ReadonlySet<strin
939
894
  return new Set([...written].filter((id) => !readSome.has(id)));
940
895
  }
941
896
 
942
- /**
943
- * The sweep of a run read once, under one set of marks. Where a question went unanswered,
944
- * the run is swept once more without what that question could have kept live and without
945
- * the references that stood in for its answer, and every declaration that sweep judges
946
- * dead and this one judges live, or dead by another rule, is held by the gap too, as is
947
- * every declaration that makes such a reference.
948
- */
897
+ /** The sweep of a run read once, under one set of marks. */
949
898
  function sweptOf<Extra>(
950
899
  read: ReadRun<Extra>,
951
- input: Pick<SweepInput, "marked" | "mode">,
900
+ input: Pick<SweepPolicy, "marked" | "mode">,
952
901
  ): RunSweep {
953
- const { swept, result } = holdingSweep(read, input);
954
- const exempt = swept.exempt ?? [];
955
- const held = new Set(read.unansweredHeld);
956
- if (held.size > 0) {
957
- // What a declaration reads where the checker did not answer is unknown, so a rule
958
- // over what it reads, the test of dead code among them, cannot judge it.
959
- for (const { configured } of read.projects) {
960
- for (const reference of configured.references) {
961
- if (reference.guessed === true) {
962
- held.add(reference.from);
963
- }
964
- }
965
- }
966
- const dead = new Map(result.candidates.map((candidate) => [candidate.id, candidate]));
967
- const answered = matrixOf(
968
- read.projects.map(({ configured }) => ({
969
- ...configured,
970
- references: configured.references.filter((one) => one.guessed !== true),
971
- })),
972
- );
973
- const bare: SweepInput = {
974
- ...swept,
975
- exempt: exempt.filter((record) => !isUnansweredRecord(record)),
976
- unanswered: [],
977
- };
978
- for (const candidate of sweepMatrix(answered, bare).candidates) {
979
- const kept = dead.get(candidate.id);
980
- if (
981
- kept?.testOfDeadCode !== candidate.testOfDeadCode ||
982
- kept.relation !== candidate.relation
983
- ) {
984
- held.add(candidate.id);
985
- }
986
- }
987
- }
902
+ const componentFiles = [...new Set(read.projects.flatMap((one) => one.componentFiles))].sort(
903
+ bytewise,
904
+ );
988
905
  return {
989
906
  matrix: read.matrix,
990
- sweep: result,
991
- exempt,
992
- retained: retainedIn(read.matrix, swept, writtenOnly(read, input.mode)),
907
+ sweep: sweep(read.matrix, {
908
+ ...input,
909
+ exempt: read.exempt,
910
+ unanswered: read.unansweredHeld,
911
+ componentFiles,
912
+ unseenCallers: !read.declaresConsumers,
913
+ writeOnly: writtenOnly(read, input.mode),
914
+ }),
993
915
  notBuilt: read.notBuilt,
994
916
  unanswered: read.unanswered,
995
- heldByUnanswered: [...held],
996
917
  skipped: read.projects.flatMap((one) => one.skipped),
997
- componentFiles: [...new Set(read.projects.flatMap((one) => one.componentFiles))].sort(bytewise),
998
918
  };
999
919
  }
1000
920
 
@@ -1011,7 +931,7 @@ export function runSweep(
1011
931
  host: Host,
1012
932
  scope: Scope,
1013
933
  config: ResolvedConfig,
1014
- input: Pick<SweepInput, "marked" | "mode">,
934
+ input: Pick<SweepPolicy, "marked" | "mode">,
1015
935
  detectors: Detectors = DETECTORS,
1016
936
  ): RunSweep {
1017
937
  return sweptOf(
@@ -1035,6 +955,7 @@ interface EmitterExtra {
1035
955
  readonly declarationsNamed: readonly boolean[];
1036
956
  readonly parts: ProjectParts;
1037
957
  readonly unseenLoads: readonly UnseenLoad[];
958
+ readonly carriers: Carriers;
1038
959
  }
1039
960
 
1040
961
  /** The project's own files below the target root that are no test file. */
@@ -1090,6 +1011,8 @@ export function readEmitterRun(
1090
1011
  declarationsNamed: declarationsNamed(project, read.held, configuredDeclarations(config)),
1091
1012
  parts: projectParts(project, read.held, references, targetRoot),
1092
1013
  unseenLoads,
1014
+ // Only a closed world reports an unnecessary export, which is all the carriers decide.
1015
+ carriers: config.consumers.complete ? carriersOf(project, read.held) : NO_CARRIERS,
1093
1016
  }),
1094
1017
  );
1095
1018
  // Whether a declaration carries the marker decides its code, so one whose answer went
@@ -1156,7 +1079,7 @@ export function emitterInputOver(
1156
1079
  scope: Scope,
1157
1080
  config: ResolvedConfig,
1158
1081
  read: ReadRun<EmitterExtra>,
1159
- input: Pick<SweepInput, "marked" | "mode">,
1082
+ input: Pick<SweepPolicy, "marked" | "mode">,
1160
1083
  selfCheck: SelfCheckFacts,
1161
1084
  ): EmitterInput {
1162
1085
  const targetRoot = scope.target.path;
@@ -1173,7 +1096,7 @@ export function emitterInputOver(
1173
1096
  support: new Set(one.configured.supportFiles ?? []),
1174
1097
  accessors: one.extra.accessors,
1175
1098
  })),
1176
- swept.exempt,
1099
+ swept.sweep.exempt,
1177
1100
  input.mode,
1178
1101
  ),
1179
1102
  dependencies: dependenciesOf(
@@ -1193,6 +1116,16 @@ export function emitterInputOver(
1193
1116
  generated: new Set(projects.flatMap((one) => [...one.extra.generated.keys()])),
1194
1117
  inOperands: new Set(projects.flatMap((one) => one.extra.inOperands)),
1195
1118
  unseenLoads: projects.flatMap((one) => one.extra.unseenLoads),
1119
+ unwrittenRuns: new Set(
1120
+ projects.flatMap((one) =>
1121
+ one.heldScripts
1122
+ .filter((held) => held.unwritten)
1123
+ .map(
1124
+ (held) =>
1125
+ relativePath(resolvePath(host.workingDirectory(), targetRoot), held.dir) ?? ".",
1126
+ ),
1127
+ ),
1128
+ ),
1196
1129
  },
1197
1130
  boundary: {
1198
1131
  consumers: {
@@ -1203,12 +1136,23 @@ export function emitterInputOver(
1203
1136
  edges: readEdgeSides(host, targetRoot),
1204
1137
  filledOutside: new Set(projects.flatMap((one) => one.filledOutside)),
1205
1138
  possiblyLoaded: new Set(projects.flatMap((one) => one.possiblyLoaded)),
1139
+ carriers: {
1140
+ named: new Map(
1141
+ [
1142
+ ...Map.groupBy(
1143
+ projects.flatMap((one) => [...one.extra.carriers.named]),
1144
+ ([id]) => id,
1145
+ ),
1146
+ ].map(([id, entries]) => [id, [...new Set(entries.flatMap(([, named]) => named))]]),
1147
+ ),
1148
+ unread: new Set(projects.flatMap((one) => [...one.extra.carriers.unread])),
1149
+ },
1206
1150
  },
1207
1151
  selfCheck,
1208
1152
  intraFunction: intraFunctionFacts(
1209
1153
  projects.map((one) => one.extra.parts),
1210
1154
  projects.flatMap((one) => one.configured.references),
1211
- new Set([...swept.exempt.map((one) => one.id), ...swept.heldByUnanswered]),
1155
+ new Set([...swept.sweep.exempt.map((one) => one.id), ...swept.sweep.held]),
1212
1156
  ),
1213
1157
  };
1214
1158
  }
@@ -1448,14 +1392,16 @@ export function runAnalysis(
1448
1392
  * declarations of its own the run holds for them.
1449
1393
  */
1450
1394
  export function unansweredCounts(analysis: RunAnalysis): readonly UnansweredCount[] {
1451
- const held = new Set(analysis.swept.heldByUnanswered);
1395
+ const held = new Set(analysis.swept.sweep.held);
1452
1396
  return analysis.run.projects.flatMap((project) => {
1453
1397
  const questions =
1454
1398
  analysis.run.unanswered.filter((one) => one.configFile === project.configFile).length +
1455
1399
  analysis.run.unread.filter(
1456
1400
  (one) => one.kind === "manifest" && one.configFile === project.configFile,
1457
1401
  ).length +
1458
- analysis.run.heldScripts.filter((one) => one.configFile === project.configFile).length;
1402
+ analysis.run.heldScripts.filter(
1403
+ (one) => one.configFile === project.configFile && one.files.length > 0,
1404
+ ).length;
1459
1405
 
1460
1406
  if (questions === 0) {
1461
1407
  return [];
@@ -14,7 +14,7 @@ import {
14
14
  type Node,
15
15
  } from "@typescript/native/ast";
16
16
  import { nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
17
- import { referenceKey } from "./matrix.ts";
17
+ import { referenceKey } from "./liveness/matrix.ts";
18
18
  import type { Reference } from "./references.ts";
19
19
  import type { ProjectView } from "./session.ts";
20
20
  import type { SourceFiles } from "./source-files.ts";