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

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 (60) hide show
  1. package/dist/src/analysis.js +26 -71
  2. package/dist/src/augmentations.js +1 -1
  3. package/dist/src/calls.js +267 -48
  4. package/dist/src/conformance-record.js +1 -1
  5. package/dist/src/contract/contract.generated.js +2 -2
  6. package/dist/src/exempt.js +0 -26
  7. package/dist/src/exposure.js +400 -0
  8. package/dist/src/findings/completion.js +4 -17
  9. package/dist/src/findings/emitters.js +22 -33
  10. package/dist/src/findings/intra-function.js +3 -3
  11. package/dist/src/findings/reads-and-writes.js +71 -15
  12. package/dist/src/findings/unused-declarations.js +2 -22
  13. package/dist/src/findings/visibility-narrowing.js +14 -20
  14. package/dist/src/inventory.js +18 -0
  15. package/dist/src/{components.js → liveness/components.js} +1 -1
  16. package/dist/src/{graph.js → liveness/graph.js} +38 -21
  17. package/dist/src/liveness/matrix.js +99 -0
  18. package/dist/src/liveness/sweep.js +558 -0
  19. package/dist/src/query.js +46 -0
  20. package/dist/src/references/sink.js +87 -1
  21. package/dist/src/references.js +1 -1
  22. package/dist/src/roots/published-reach.js +4 -4
  23. package/dist/src/verbs/explain.js +73 -152
  24. package/dist/src/verbs/print-retained.js +1 -1
  25. package/jsr.json +1 -1
  26. package/package.json +1 -1
  27. package/src/analysis.ts +41 -107
  28. package/src/augmentations.ts +1 -1
  29. package/src/calls.ts +326 -52
  30. package/src/conformance-record.ts +1 -1
  31. package/src/contract/contract.generated.ts +2 -2
  32. package/src/exempt.ts +31 -46
  33. package/src/exposure.ts +492 -0
  34. package/src/finding.ts +1 -1
  35. package/src/findings/boundary.ts +7 -0
  36. package/src/findings/completion.ts +5 -19
  37. package/src/findings/emitters.ts +23 -41
  38. package/src/findings/interfaces.ts +3 -3
  39. package/src/findings/intra-function.ts +3 -3
  40. package/src/findings/reads-and-writes.ts +99 -26
  41. package/src/findings/unused-declarations.ts +4 -28
  42. package/src/findings/visibility-narrowing.ts +25 -26
  43. package/src/interface-satisfaction.ts +1 -1
  44. package/src/inventory.ts +19 -0
  45. package/src/{components.ts → liveness/components.ts} +3 -3
  46. package/src/{graph.ts → liveness/graph.ts} +74 -26
  47. package/src/liveness/matrix.ts +162 -0
  48. package/src/liveness/sweep.ts +855 -0
  49. package/src/query.ts +81 -0
  50. package/src/references/sink.ts +101 -0
  51. package/src/references.ts +8 -1
  52. package/src/roots/published-reach.ts +9 -4
  53. package/src/stores.ts +2 -1
  54. package/src/tests/file-class.ts +19 -2
  55. package/src/verbs/explain.ts +81 -199
  56. package/src/verbs/print-retained.ts +1 -1
  57. package/dist/src/matrix.js +0 -275
  58. package/dist/src/sweep.js +0 -234
  59. package/src/matrix.ts +0 -466
  60. package/src/sweep.ts +0 -384
@@ -1,19 +1,17 @@
1
1
  import { EXIT_CLEAN, EXIT_USAGE } from "../contract/contract.generated.ts";
2
2
  import { productionMode, runAnalysis, type RunAnalysis } from "../analysis.ts";
3
- import { listing } from "../components.ts";
3
+ import { listing } from "../liveness/components.ts";
4
4
  import type { Cascade } from "../config.ts";
5
5
  import type { CompletedFinding } from "../finding.ts";
6
- import { OUTSIDE, swept as judged } from "../graph.ts";
6
+ import { swept as judged, type Edge, type Graph } from "../liveness/graph.ts";
7
+ import type { Origin } from "../liveness/sweep.ts";
7
8
  import type { InventorySymbol } from "../inventory.ts";
8
- import { referenceKey, type Configured } from "../matrix.ts";
9
9
  import { partialNotes } from "../partial.ts";
10
10
  import { resolvePath } from "../paths.ts";
11
11
  import { positionKey } from "../position.ts";
12
12
  import type { Reference } from "../references.ts";
13
- import type { Mode } from "../sweep.ts";
14
- import { madeByTestCode, withheldBy } from "../tests/file-class.ts";
13
+ import { madeByTestCode } from "../tests/file-class.ts";
15
14
  import { resolve } from "../resolve.ts";
16
- import type { Root } from "../roots/root-set.ts";
17
15
  import { type Verb, type VerbOptions } from "./verb.ts";
18
16
  import { UsageError } from "../usage.ts";
19
17
  import { bytewise } from "../text/compare.ts";
@@ -106,160 +104,27 @@ function subjectOf(
106
104
  return { candidates: partial.slice(0, MAX_PARTIAL_MATCHES) };
107
105
  }
108
106
 
109
- /** Every reference of the run once, a reference several configurations make counted once. */
110
- function referencesOf(configured: readonly Configured[]): readonly Reference[] {
111
- const seen = new Set<string>();
112
- const held: Reference[] = [];
113
- for (const reference of configured.flatMap((one) => one.references)) {
114
- const key = referenceKey(reference);
115
- if (!seen.has(key)) {
116
- seen.add(key);
117
- held.push(reference);
118
- }
119
- }
120
- return held;
121
- }
122
-
123
- /** Every root of the run once per reason, in the order the projects name them. */
124
- function rootsOf(configured: readonly Configured[]): readonly Root[] {
125
- const seen = new Set<string>();
126
- return configured
127
- .flatMap((one) => one.roots)
128
- .filter((root) => {
129
- const key = [root.id, root.kind, root.source].join("\u0000");
130
- if (seen.has(key)) {
131
- return false;
132
- }
133
- seen.add(key);
134
- return true;
135
- });
136
- }
137
-
138
- /**
139
- * One step of a path: a reference, or a reference that reads a module and so reaches a
140
- * declaration the module exports.
141
- */
142
- type Hop =
143
- | { readonly k: "reference"; readonly reference: Reference }
144
- | { readonly k: "export"; readonly reference: Reference; readonly to: string };
145
-
146
- /**
147
- * Where a path starts: a root of the run, or a reference a loaded consumer or a test file no
148
- * project holds makes.
149
- */
150
- type Origin =
151
- | { readonly k: "root"; readonly root: Root }
152
- | { readonly k: "consumer"; readonly consumer: string }
153
- | { readonly k: "unheld"; readonly file: string };
154
-
155
- /**
156
- * The shortest path from a root, a loaded consumer or a test file no project holds to one
157
- * declaration, walked breadth first from every root at once in the order the run names them,
158
- * a reference of a consumer or of such a file being the first hop of a path of its own, as
159
- * the sweep seeds them, and following the references the sweep follows: a
160
- * reference that reads a module reaches what the module exports, and an evaluation or a root
161
- * reaches the module alone. Under `production` no reference a test file made is followed,
162
- * which is the set the report's sweep counts.
163
- */
164
- function shortestPath(
165
- analysis: RunAnalysis,
166
- to: string,
167
- mode: Mode,
168
- support: ReadonlySet<string>,
169
- ): { readonly origin: Origin; readonly path: readonly Hop[] } | undefined {
170
- const graph = analysis.swept.matrix.union;
171
- const out = new Map<string, Reference[]>();
172
- const consumed: { readonly reference: Reference; readonly origin: Origin }[] = [];
173
- for (const reference of referencesOf(analysis.configured)) {
174
- if (withheldBy(mode, reference, support)) {
175
- continue;
176
- }
177
- const { consumer } = reference;
178
- if (consumer !== undefined) {
179
- consumed.push({ reference, origin: { k: "consumer", consumer } });
180
- continue;
181
- }
182
- if (reference.unheld === true) {
183
- consumed.push({
184
- reference,
185
- origin: { k: "unheld", file: reference.from.replace(/#$/u, "") },
186
- });
187
- continue;
188
- }
189
- const held = out.get(reference.from);
190
- if (held === undefined) {
191
- out.set(reference.from, [reference]);
192
- } else {
193
- held.push(reference);
194
- }
195
- }
196
- interface Step {
197
- readonly at: string;
198
- readonly hop: Hop | undefined;
199
- readonly previous: number;
200
- readonly origin: Origin;
201
- }
202
- const steps: Step[] = [];
203
- const seen = new Set<string>();
204
- const expanded = new Set<string>();
205
- const enter = (step: Step): void => {
206
- if (!seen.has(step.at)) {
207
- seen.add(step.at);
208
- steps.push(step);
209
- }
210
- };
211
- const follow = (reference: Reference, previous: number, origin: Origin): void => {
212
- enter({ at: reference.to, hop: { k: "reference", reference }, previous, origin });
213
- const target = graph.at(reference.to);
214
- if (reference.use === "evaluation" || target === OUTSIDE || expanded.has(reference.to)) {
215
- return;
216
- }
217
- expanded.add(reference.to);
218
- for (const exported of graph.exportsOf[target] ?? []) {
219
- const id = graph.symbols[exported]?.id;
220
- if (id !== undefined) {
221
- enter({ at: id, hop: { k: "export", reference, to: id }, previous, origin });
222
- }
223
- }
224
- };
225
- for (const root of rootsOf(analysis.configured)) {
226
- enter({ at: root.id, hop: undefined, previous: -1, origin: { k: "root", root } });
227
- }
228
- // A consumed reference is one hop long, so it joins the walk where the roots' first hops do.
229
- const firstHops = steps.length;
230
- for (let head = 0; ; head += 1) {
231
- if (head === firstHops) {
232
- for (const { reference, origin } of consumed) {
233
- follow(reference, -1, origin);
234
- }
235
- }
236
- const step = steps[head];
237
- if (step === undefined) {
238
- break;
239
- }
240
- if (step.at === to) {
241
- const path: Hop[] = [];
242
- for (let at: Step | undefined = step; at?.hop !== undefined; at = steps[at.previous]) {
243
- path.unshift(at.hop);
244
- }
245
- return { origin: step.origin, path };
246
- }
247
- for (const reference of out.get(step.at) ?? []) {
248
- follow(reference, head, step.origin);
249
- }
250
- }
251
- return undefined;
252
- }
253
-
254
107
  /** Where a path starts, as a line names it. */
255
- function originText(origin: Origin): string {
108
+ function originText(origin: Origin, graph: Graph): string {
109
+ const id = (at: number): string => graph.symbols[at]?.id ?? "";
256
110
  switch (origin.k) {
257
111
  case "root":
258
- return `root ${origin.root.kind} ${origin.root.id}`;
259
- case "consumer":
260
- return `consumer ${origin.consumer}`;
261
- case "unheld":
262
- return `test file no project holds ${origin.file}`;
112
+ return `root ${origin.rooted.kind} ${id(origin.rooted.at)}`;
113
+ case "consumed": {
114
+ const reference = graph.references[origin.edge.reference];
115
+ if (reference?.consumer !== undefined) {
116
+ return `consumer ${reference.consumer}`;
117
+ }
118
+ return reference?.unheld === true
119
+ ? `test file no project holds ${reference.from.replace(/#$/u, "")}`
120
+ : `declaration no configuration judges ${reference?.from ?? ""}`;
121
+ }
122
+ case "exemption":
123
+ return `exemption ${origin.record.class} ${positionKey(origin.record.site)}`;
124
+ case "mark":
125
+ return `suppression of ${id(origin.at)}`;
126
+ case "unanswered":
127
+ return `unanswered question about ${id(origin.at)}`;
263
128
  }
264
129
  }
265
130
 
@@ -268,52 +133,74 @@ function madeBy(reference: Reference): string {
268
133
  return reference.consumer === undefined ? reference.from : `consumer ${reference.consumer}`;
269
134
  }
270
135
 
271
- /** One reference of an answer, marked where test code made it, a test-support file's included. */
272
- function referenceLine(reference: Reference, tail: string, support: ReadonlySet<string>): string {
273
- return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${madeByTestCode(reference, support) ? "\tfrom a test file" : ""}\n`;
136
+ /**
137
+ * One reference of an answer, marked where test code made it, a test-support file's included,
138
+ * and naming the configurations that make it where they are not every one of the run.
139
+ */
140
+ function referenceLine(
141
+ reference: Reference,
142
+ tail: string,
143
+ support: ReadonlySet<string>,
144
+ configurations: { readonly making: readonly string[]; readonly run: readonly string[] },
145
+ ): string {
146
+ const partial = configurations.making.length < configurations.run.length;
147
+ return ` reference: ${positionKey(reference.position)}\t${reference.use}\t${tail}${madeByTestCode(reference, support) ? "\tfrom a test file" : ""}${partial ? `\tmade in ${configurations.making.join(" ")}` : ""}\n`;
274
148
  }
275
149
 
276
150
  /** Why a declaration no finding names and no exemption held back is live, or how it fell. */
277
- function liveness(
278
- analysis: RunAnalysis,
279
- subject: InventorySymbol,
280
- cascade: Cascade,
281
- mode: Mode,
282
- ): string {
283
- const { sweep } = analysis.swept;
151
+ function liveness(analysis: RunAnalysis, subject: InventorySymbol, cascade: Cascade): string {
152
+ const { matrix, sweep } = analysis.swept;
153
+ const graph = matrix.union;
284
154
  const candidate = sweep.candidates.find((one) => one.id === subject.id);
285
155
  if (candidate === undefined && !judged(subject.kind)) {
286
156
  return `answer: unjudged\n unjudged: neither liveness relation judges a ${subject.kind}\n`;
287
157
  }
288
158
  let text = `answer: ${candidate === undefined ? "live" : "dead"}\n`;
289
159
  text += ` live under: ${listed(sweep.liveUnder.get(subject.id) ?? [])}\n`;
290
- for (const root of rootsOf(analysis.configured).filter((one) => one.id === subject.id)) {
291
- text += ` root: ${root.kind}${root.source === "" ? "" : `\tnamed by ${root.source}`}\n`;
160
+ const at = graph.at(subject.id);
161
+ const seen = new Set<string>();
162
+ for (const root of graph.rooted.filter((one) => one.at === at)) {
163
+ const key = `${root.kind}\u0000${root.source}`;
164
+ if (!seen.has(key)) {
165
+ seen.add(key);
166
+ text += ` root: ${root.kind}${root.source === "" ? "" : `\tnamed by ${root.source}`}\n`;
167
+ }
292
168
  }
293
169
  const support = new Set(analysis.configured.flatMap((one) => one.supportFiles ?? []));
294
- const found =
295
- shortestPath(analysis, subject.id, mode, support) ??
296
- shortestPath(analysis, subject.id, { production: false }, support);
297
- if (found !== undefined && found.path.length > 0) {
298
- const { origin } = found;
299
- text += ` reached from: ${originText(origin)}\n`;
300
- for (const hop of found.path) {
301
- text += referenceLine(
302
- hop.reference,
303
- `${madeBy(hop.reference)} -> ${hop.reference.to}`,
304
- support,
305
- );
170
+ const line = (edge: Edge, tail: (reference: Reference) => string): string => {
171
+ const reference = graph.references[edge.reference];
172
+ return reference === undefined
173
+ ? ""
174
+ : referenceLine(reference, tail(reference), support, {
175
+ making: graph.configurations[edge.reference] ?? [],
176
+ run: matrix.configurations,
177
+ });
178
+ };
179
+ const found = sweep.path(subject.id);
180
+ if (found !== undefined && found.hops.length > 0) {
181
+ text += ` reached from: ${originText(found.origin, graph)}\n`;
182
+ for (const hop of found.hops) {
183
+ text += line(hop.edge, (reference) => `${madeBy(reference)} -> ${reference.to}`);
306
184
  if (hop.k === "export") {
307
- text += ` export: ${hop.reference.to} -> ${hop.to}\n`;
185
+ const reference = graph.references[hop.edge.reference];
186
+ text += ` export: ${reference?.to ?? ""} -> ${graph.symbols[hop.to]?.id ?? ""}\n`;
308
187
  }
309
188
  }
310
189
  } else if (found === undefined) {
311
- const references = referencesOf(analysis.configured).filter((one) => one.to === subject.id);
190
+ const references = graph.references.flatMap((one, place) =>
191
+ one.to === subject.id ? [place] : [],
192
+ );
312
193
  text +=
313
194
  " unreachable: no path of references reaches it from a root, a loaded consumer or a test file no project holds\n";
314
195
  text += ` references: ${String(references.length)}\n`;
315
- for (const reference of references) {
316
- text += referenceLine(reference, `from ${madeBy(reference)}`, support);
196
+ for (const place of references) {
197
+ const reference = graph.references[place];
198
+ if (reference !== undefined) {
199
+ text += referenceLine(reference, `from ${madeBy(reference)}`, support, {
200
+ making: graph.configurations[place] ?? [],
201
+ run: matrix.configurations,
202
+ });
203
+ }
317
204
  }
318
205
  }
319
206
  if (candidate !== undefined) {
@@ -371,12 +258,7 @@ function namedElsewhere(findings: readonly CompletedFinding[], subject: Inventor
371
258
  }
372
259
 
373
260
  /** The one explanation that applies to a declaration, built whole before it is written. */
374
- function explanation(
375
- analysis: RunAnalysis,
376
- subject: InventorySymbol,
377
- cascade: Cascade,
378
- mode: Mode,
379
- ): string {
261
+ function explanation(analysis: RunAnalysis, subject: InventorySymbol, cascade: Cascade): string {
380
262
  const graph = analysis.swept.matrix.union;
381
263
  const at = graph.at(subject.id);
382
264
  let text = `symbol: ${subject.ref}\n`;
@@ -384,7 +266,7 @@ function explanation(
384
266
  text += `position: ${subject.id}\n`;
385
267
  text += `configurations: ${listed(analysis.swept.matrix.heldIn[at] ?? [])}\n`;
386
268
  const found = analysis.result.findings.find((one) => positionKey(one.position) === subject.id);
387
- const retained = analysis.swept.retained.filter((one) => one.id === subject.id);
269
+ const retained = analysis.swept.sweep.retained.filter((one) => one.id === subject.id);
388
270
  if (found !== undefined) {
389
271
  text += reported(found);
390
272
  } else if (retained.length > 0) {
@@ -392,11 +274,11 @@ function explanation(
392
274
  for (const one of retained) {
393
275
  text += ` class: ${one.class}\t${positionKey(one.site)}\t${one.detail}\n`;
394
276
  }
395
- } else if (analysis.swept.heldByUnanswered.includes(subject.id)) {
277
+ } else if (analysis.swept.sweep.held.includes(subject.id)) {
396
278
  text +=
397
279
  "answer: held\n held by: a question the checker did not answer, which could have kept it live, so no finding names it\n";
398
280
  } else {
399
- text += liveness(analysis, subject, cascade, mode);
281
+ text += liveness(analysis, subject, cascade);
400
282
  }
401
283
  return text + namedElsewhere(analysis.result.findings, subject);
402
284
  }
@@ -404,10 +286,10 @@ function explanation(
404
286
  /**
405
287
  * Explains one symbol from the analysis the report is built from: why a reported symbol is
406
288
  * reported, which exemption classes held a retained one back, that an unanswered question held
407
- * a held one, and for any other declaration the relations that hold it live and the shortest
408
- * path from a root, a loaded consumer or a test file no project holds, or the references it
409
- * has where no path reaches it. A request naming no one declaration exits with the usage
410
- * code and names what partially matches it. Where the analysis was partial, the error stream says so.
289
+ * a held one, and for any other declaration the relations that hold it live and the path the
290
+ * sweep's strongest tier reaching it follows, or the references it has where no path reaches
291
+ * it. A request naming no one declaration exits with the usage code and names what partially
292
+ * matches it. Where the analysis was partial, the error stream says so.
411
293
  */
412
294
  export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, option }) => {
413
295
  const named = namedSymbol(option);
@@ -430,7 +312,7 @@ export const explainVerb: Verb = ({ out, err, host, inputs, scope, openClient, o
430
312
  err.write(" no symbol of the target partially matches it either\n");
431
313
  }
432
314
  } else {
433
- out.write(explanation(analysis, subject, config.reporters.cascade, mode));
315
+ out.write(explanation(analysis, subject, config.reporters.cascade));
434
316
  }
435
317
  for (const note of notes) {
436
318
  diagnose(err, note);
@@ -21,7 +21,7 @@ export const printRetainedVerb: Verb = ({ out, err, host, inputs, scope, openCli
21
21
  marked: [],
22
22
  mode: productionMode(config),
23
23
  });
24
- for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
24
+ for (const line of retainedLines(swept.matrix.union.symbols, swept.sweep.retained)) {
25
25
  out.write(`${line}\n`);
26
26
  }
27
27
  for (const note of partialNotes({
@@ -1,275 +0,0 @@
1
- /**
2
- * One run's projects as one matrix: every declaration any project holds, keyed by its
3
- * position and carrying the configurations that hold it, swept once per configuration
4
- * and intersected, with the dead components computed once over every configuration.
5
- */
6
- import { componentsOf } from "./components.js";
7
- import { graphOf, OUTSIDE } from "./graph.js";
8
- import { byPosition } from "./position.js";
9
- import { edgeWithheldBy } from "./tests/file-class.js";
10
- import { reachedWithTheTests, splitRefs, sweep, } from "./sweep.js";
11
- /**
12
- * What makes one reference one reference, whichever configuration made it. A consumer's
13
- * position is rendered against its own root, so the consumer is part of what it names.
14
- */
15
- export function referenceKey(reference) {
16
- const { path, line, column } = reference.position;
17
- return [
18
- reference.consumer ?? "",
19
- reference.from,
20
- reference.to,
21
- path,
22
- String(line),
23
- String(column),
24
- reference.use,
25
- ].join("\u0000");
26
- }
27
- /**
28
- * Indexes what the passes answered for each configuration of one run, in the run's order.
29
- * A declaration is keyed by its rendered position, with the fields of the first
30
- * configuration that judges it; one in a file every holder holds only for its references
31
- * is judged by none and is not in the union. One reference at one position of a file
32
- * several configurations compile is one reference; two one configuration makes are two.
33
- */
34
- export function matrixOf(per) {
35
- const symbols = [];
36
- const holders = new Map();
37
- const references = [];
38
- const firstSeen = new Map();
39
- const judged = new Set();
40
- const dependents = new Map();
41
- per.forEach((one, index) => {
42
- for (const [id, after] of one.dependents ?? []) {
43
- if (!dependents.has(id)) {
44
- dependents.set(id, after);
45
- }
46
- }
47
- const referenceOnly = new Set(one.referenceOnly);
48
- for (const symbol of one.symbols) {
49
- const held = holders.get(symbol.id);
50
- if (held === undefined) {
51
- holders.set(symbol.id, [one.configuration]);
52
- }
53
- else if (!held.includes(one.configuration)) {
54
- held.push(one.configuration);
55
- }
56
- if (!referenceOnly.has(symbol.position.path) && !judged.has(symbol.id)) {
57
- judged.add(symbol.id);
58
- symbols.push(symbol);
59
- }
60
- }
61
- for (const reference of one.references) {
62
- const key = referenceKey(reference);
63
- const seen = firstSeen.get(key);
64
- if (seen === undefined) {
65
- firstSeen.set(key, index);
66
- }
67
- else if (seen !== index) {
68
- continue;
69
- }
70
- references.push(reference);
71
- }
72
- });
73
- symbols.sort((a, b) => byPosition(a.position, b.position));
74
- const testUsed = new Set(per.flatMap((one) => one.testUsedFiles ?? []));
75
- return {
76
- configurations: per.map((one) => one.configuration),
77
- graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles, one.supportFiles, one.ambient)),
78
- union: graphOf(symbols, references, per.flatMap((one) => one.roots), [...new Set(per.flatMap((one) => one.testFiles))], [...new Set(per.flatMap((one) => one.supportFiles ?? []))], new Set(per.flatMap((one) => [...(one.ambient ?? [])]))),
79
- heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
80
- testUsed: symbols.map((symbol) => testUsed.has(symbol.position.path)),
81
- dependents,
82
- };
83
- }
84
- /**
85
- * Which declarations of the run are dead in every configuration that holds them. Each
86
- * configuration is swept on its own graph, since a file it does not compile references nothing
87
- * there and a root it does not hold seeds nothing. A declaration is a candidate when every
88
- * configuration holding it judged it one; its relation is reference counting where every one
89
- * found it so, else reachability, and its counts are the union's. Components are computed once,
90
- * over the union and the intersected dead set, so one dead component has one identifier.
91
- */
92
- export function sweepMatrix(matrix, input) {
93
- const per = matrix.graphs.map((graph) => sweep(graph, input));
94
- const byConfiguration = new Map(matrix.configurations.map((configuration, index) => [
95
- configuration,
96
- new Map((per[index]?.candidates ?? []).map((candidate) => [candidate.id, candidate])),
97
- ]));
98
- const union = matrix.union;
99
- const heldBy = union.symbols.map(() => undefined);
100
- const held = [
101
- ...input.marked,
102
- ...(input.exempt ?? []).map((record) => record.id),
103
- ...(input.unanswered ?? []),
104
- ];
105
- const testReached = reachedWithTheTests(union, held);
106
- // A test uses what it names: a file a test reads through a star re-export or a
107
- // namespace keeps the exports nothing names at their own class.
108
- const testNamed = reachedWithTheTests(union, held, false);
109
- const everywhere = union.symbols.map((symbol, at) => {
110
- const held = matrix.heldIn[at] ?? [];
111
- const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
112
- return held.length === 0 || found.some((candidate) => candidate === undefined)
113
- ? undefined
114
- : found.filter((candidate) => candidate !== undefined);
115
- });
116
- const compiled = compiledReferences(matrix, new Set(union.symbols.flatMap((symbol, at) => (everywhere[at] === undefined ? [] : [symbol.id]))), input.mode);
117
- const judged = everywhere.map((found, at) => compiled.has(union.symbols[at]?.id ?? "") ? undefined : found);
118
- // A configuration that does not hold a test's subject's caller finds the subject dead,
119
- // so a test is of dead code only where every production subject it names is dead in
120
- // every configuration that holds it.
121
- const subjectsDead = (at) => (union.out[at] ?? []).every((edge) => union.test[edge.to] === true ||
122
- union.support[edge.to] === true ||
123
- union.ambient[edge.to] === true ||
124
- union.subject[edge.to] !== true ||
125
- judged[edge.to] !== undefined);
126
- const admittedAt = (at) => judged[at]?.every((candidate) => candidate.testOfDeadCode) === true && subjectsDead(at);
127
- const kept = keptByTests(union, testReached, admittedAt);
128
- const testUsed = testUsedReach(union, union.symbols.map((_symbol, at) => matrix.testUsed[at] === true && testNamed[at] === true), judged.map((one) => one !== undefined));
129
- const candidates = [];
130
- union.symbols.forEach((symbol, at) => {
131
- const all = judged[at];
132
- if (all === undefined) {
133
- return;
134
- }
135
- const held = matrix.heldIn[at] ?? [];
136
- const admitted = admittedAt(at);
137
- const split = splitRefs(union.made[at], input.mode);
138
- const testRefs = split.test;
139
- const heldByTests = heldBy[union.parent[at] ?? OUTSIDE] ??
140
- (union.support[at] === true && testRefs > 0 && !admitted ? symbol.id : undefined);
141
- const unreferenced = union.test[at] === true && !admitted && testReached[at] !== true;
142
- heldBy[at] = heldByTests;
143
- const productionRefs = split.production;
144
- const counted = all.every((candidate) => candidate.relation === "reference-counting");
145
- candidates.push({
146
- id: symbol.id,
147
- productionRefs,
148
- testRefs,
149
- // The run that judges a test-file declaration counts its test references.
150
- relation: (unreferenced ? productionRefs + testRefs === 0 : counted)
151
- ? "reference-counting"
152
- : "reachability",
153
- testOfDeadCode: admitted,
154
- configurations: held,
155
- heldByTests,
156
- unreferencedTest: unreferenced,
157
- testUsed: testUsed[at] === true,
158
- keptByTests: kept[at] === true,
159
- });
160
- });
161
- const liveUnder = liveAnywhere(union.symbols, per);
162
- return {
163
- liveUnder,
164
- candidates,
165
- components: deadComponents(matrix, { liveUnder, candidates }, new Set()),
166
- };
167
- }
168
- /**
169
- * Every declaration of `dead`, those dead in every configuration that holds them, that a
170
- * configuration's graph references from a judged declaration outside `dead` or already
171
- * kept, until none joins: deleting one would break that configuration's compile. A
172
- * production run counts no reference a test file made.
173
- */
174
- function compiledReferences(matrix, dead, mode) {
175
- const kept = new Set();
176
- for (let changed = true; changed;) {
177
- changed = false;
178
- for (const graph of matrix.graphs) {
179
- graph.out.forEach((edges, from) => {
180
- const id = graph.symbols[from]?.id ?? "";
181
- if (graph.subject[from] !== true || (dead.has(id) && !kept.has(id))) {
182
- return;
183
- }
184
- for (const edge of edges) {
185
- const to = graph.symbols[edge.to]?.id ?? "";
186
- if (!edgeWithheldBy(mode, edge) && dead.has(to) && !kept.has(to)) {
187
- kept.add(to);
188
- changed = true;
189
- }
190
- }
191
- });
192
- }
193
- }
194
- return kept;
195
- }
196
- /** Per declaration of the union, whether one of `seeds` is it or reaches it through `dead` declarations. */
197
- function testUsedReach(union, seeds, dead) {
198
- // A file is no candidate, and its top level is what reaches what it alone calls.
199
- const reached = seeds.map((seed, at) => seed && dead[at] === true);
200
- const queue = seeds.flatMap((one, at) => (one ? [at] : []));
201
- for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
202
- for (const edge of union.out[at] ?? []) {
203
- if (dead[edge.to] === true && reached[edge.to] !== true) {
204
- reached[edge.to] = true;
205
- queue.push(edge.to);
206
- }
207
- }
208
- }
209
- return reached;
210
- }
211
- /** Per declaration of the union, whether test code `reached` holds and `admitted` rejects references it. */
212
- function keptByTests(union, reached, admitted) {
213
- const kept = union.symbols.map(() => false);
214
- union.out.forEach((edges, from) => {
215
- const testCode = union.test[from] === true || union.support[from] === true;
216
- if (!testCode || reached[from] !== true || admitted(from)) {
217
- return;
218
- }
219
- for (const edge of edges) {
220
- if (edge.test) {
221
- kept[edge.to] = true;
222
- }
223
- }
224
- });
225
- return kept;
226
- }
227
- /**
228
- * The dead components of one sweep over the union, with the file declarations at `files`
229
- * dead as well: a file that is deleted whole takes the declarations it holds and every
230
- * dead declaration only its top level references with it.
231
- */
232
- export function deadComponents(matrix, result, files) {
233
- const union = matrix.union;
234
- const byId = new Map(result.candidates.map((candidate) => [candidate.id, candidate]));
235
- const candidateAt = (at) => byId.get(union.symbols[at]?.id ?? "");
236
- return componentsOf(union, union.symbols.map((_symbol, at) => {
237
- const candidate = candidateAt(at);
238
- return files.has(at) || (candidate !== undefined && candidate.heldByTests === undefined);
239
- }), union.symbols.map((_symbol, at) => candidateAt(at)?.testOfDeadCode === true), union.symbols.map((_symbol, at) => candidateAt(at)?.unreferencedTest === true), positionsOf(union, matrix.dependents, result.liveUnder));
240
- }
241
- /**
242
- * The file declarations at `paths`, below the target root, that one sweep holds live
243
- * under both relations, the rule a judged declaration is live by.
244
- */
245
- export function liveFilesAt(matrix, result, paths) {
246
- return matrix.union.symbols.filter((symbol) => symbol.kind === "file" &&
247
- paths.has(symbol.position.path) &&
248
- (result.liveUnder.get(symbol.id)?.length ?? 0) === RELATIONS.length);
249
- }
250
- /**
251
- * Each declaration's dependents by its place in the union, and per place whether the
252
- * declaration is live under reachability in some configuration.
253
- */
254
- function positionsOf(union, dependents, liveUnder) {
255
- return {
256
- dependents: union.symbols.map((symbol) => (dependents.get(symbol.id) ?? []).map((id) => union.at(id)).filter((at) => at !== OUTSIDE)),
257
- live: union.symbols.map((symbol) => liveUnder.get(symbol.id)?.includes("reachability") === true),
258
- };
259
- }
260
- /** The relations in the order a declaration's entry lists them. */
261
- const RELATIONS = ["reference-counting", "reachability"];
262
- /**
263
- * Per declaration, in site order, every relation that holds it live in at least one
264
- * configuration.
265
- */
266
- function liveAnywhere(symbols, per) {
267
- const live = new Map();
268
- for (const symbol of symbols) {
269
- const relations = RELATIONS.filter((relation) => per.some((liveness) => (liveness.liveUnder.get(symbol.id) ?? []).includes(relation)));
270
- if (relations.length > 0) {
271
- live.set(symbol.id, relations);
272
- }
273
- }
274
- return live;
275
- }