@cplieger/deadset-ts 3.0.0 → 3.2.0

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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ deadset-ts is a command-line analyzer for TypeScript and JavaScript projects. It
8
8
 
9
9
  deadset-ts is report-only. It never edits source, and it refuses any `--fix` flag.
10
10
 
11
- **Status: pre-release.** This version implements contract 2.2.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process; no other verb reports findings yet, so `analyze`, `explain`, `print-retained` and `describe` exit with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
11
+ **Status: pre-release.** This version implements contract 3.0.0 and ships the analyzer's foundation: one compiler session per run over the projects `analysis.configurations` declares, in the order it lists them, each a compiler configuration file below the target, or, where it declares none, every `tsconfig` project discovered under the target (the explicit scope, every `tsconfig*.json`, each project's `references`), a fail-closed read of every project's diagnostics, configuration decoding against the contract's closed key list with `print-config` and its provenance, `print-projects`, the report-only guard, the symbol inventory of every declaration a project's own files hold down to its class and type members, the one-pass reference resolution over that inventory, which resolves each file's identifiers in batches ([batch-cap calibration](docs/batch-cap-calibration.md) is the measurement behind the batch size), and the root set `print-roots` prints. A root is a declaration the analysis keeps live without a reference: what a file exports when the manifest names it through `main`, `module`, `types`, `bin` or `exports`, or when `ts.entry_files` matches it; a library target's published API; the configuration, setup and test files of Vitest, Stryker and Playwright, the flat configuration of ESLint, and a worker or service worker a call addresses by a string literal; and every declaration a `roots.patterns` entry names. An entry that names nothing is reported as `DS1704` and fails the run. The entry-point conventions of other tools are not read, and `DECLINED_CONVENTIONS` lists each one with the reason. The analysis is reached through the command, and through `run()` for a caller that runs the command in-process; no other verb reports findings yet, so `analyze`, `explain` and `describe` exit with a usage message. The one runtime dependency is the `typescript` package at exactly 7.0.2, installed under the `@typescript/native` name until TypeScript 7.1 ships a compiler API the tooling can share.
12
12
 
13
13
  ## Install
14
14
 
@@ -28,7 +28,7 @@ The npm package carries the command and the library as JavaScript that the compi
28
28
  npx deadset-ts version
29
29
  ```
30
30
 
31
- `version` prints the analyzer version and the Contract version it implements, and exits 0. `print-config`, `print-projects` and `print-roots` print what their row of the table says and exit 0, or 1 when `print-roots` reports a `DS1704`. `analyze`, `explain`, `print-retained`, `describe`, any other invocation and no arguments print the usage line and exit 2.
31
+ `version` prints the analyzer version and the Contract version it implements, and exits 0. `print-config`, `print-projects`, `print-roots` and `print-retained` print what their row of the table says and exit 0, or 1 when `print-roots` reports a `DS1704`, and `print-retained` exits 2 when an `exemptions.disabled` entry is not an exemption class. `analyze`, `explain`, `describe`, any other invocation and no arguments print the usage line and exit 2.
32
32
 
33
33
  | Verb | Job |
34
34
  | ---------------- | ------------------------------------------------------------------------------- |
@@ -37,7 +37,7 @@ npx deadset-ts version
37
37
  | `print-config` | Print the resolved configuration and where each setting came from |
38
38
  | `print-projects` | Print the identifier of every project a run analyzes |
39
39
  | `print-roots` | Print every declaration the analysis keeps live without a reference, and why |
40
- | `print-retained` | Print every symbol an exemption class held back, with the class |
40
+ | `print-retained` | Print every declaration an exemption held back: each class, its site and detail |
41
41
  | `describe` | Describe the analyzer: version, Contract version, supported issue kinds |
42
42
  | `version` | Print the analyzer and Contract versions |
43
43
 
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Where an import or export chain leads in one project: from one symbol to each
3
+ * declaration of the target it is, and along the chain of aliases it heads to each
4
+ * declaration every link of that chain is.
5
+ */
6
+ import { SymbolFlags } from "@typescript/native/unstable/sync";
7
+ import { nodeKey } from "./inventory.js";
8
+ /**
9
+ * The chain walks over one project, against the declarations of `held`, the same
10
+ * project's inventory.
11
+ *
12
+ * A declaration handle carries the file it is in, so whether it is one of the
13
+ * project's own files is answered before the handle is resolved: a declaration of a
14
+ * library file is never fetched to find out that the inventory does not hold it.
15
+ */
16
+ export function aliasChains(project, held) {
17
+ const ownFiles = new Set(project.ownSourceFiles().map((file) => file.fileName));
18
+ const steps = new Map();
19
+ const chains = new Map();
20
+ let asked = 0;
21
+ const declarationsOf = (symbol) => {
22
+ const ids = [];
23
+ for (const handle of symbol.declarations) {
24
+ if (!ownFiles.has(handle.path)) {
25
+ continue;
26
+ }
27
+ const node = project.declarationAt(handle)?.node;
28
+ const id = node === undefined ? undefined : held.declarations.get(nodeKey(node.getSourceFile(), node));
29
+ if (id !== undefined && !ids.includes(id)) {
30
+ ids.push(id);
31
+ }
32
+ }
33
+ return ids;
34
+ };
35
+ /**
36
+ * What one alias names, one link along. Only a symbol the binder flagged an alias is
37
+ * asked, which is the condition the step asserts on: a default export of an
38
+ * expression is written in an alias form and names nothing declared elsewhere, so its
39
+ * chain ends at itself.
40
+ */
41
+ const stepOf = (symbol) => {
42
+ if ((symbol.flags & SymbolFlags.Alias) === 0) {
43
+ return undefined;
44
+ }
45
+ if (steps.has(symbol.id)) {
46
+ return steps.get(symbol.id);
47
+ }
48
+ const next = project.aliasStepOf(symbol);
49
+ asked += 1;
50
+ steps.set(symbol.id, next);
51
+ return next;
52
+ };
53
+ const chainOf = (symbol) => {
54
+ const cached = chains.get(symbol.id);
55
+ if (cached !== undefined) {
56
+ return cached;
57
+ }
58
+ const targets = [];
59
+ const walked = new Set();
60
+ let at = symbol;
61
+ let stepped = false;
62
+ while (at !== undefined && !walked.has(at.id)) {
63
+ walked.add(at.id);
64
+ for (const id of declarationsOf(at)) {
65
+ if (!targets.some((target) => target.id === id)) {
66
+ targets.push({ id, stepped });
67
+ }
68
+ }
69
+ at = stepOf(at);
70
+ stepped = true;
71
+ }
72
+ chains.set(symbol.id, targets);
73
+ return targets;
74
+ };
75
+ const nextDeclared = (symbol) => {
76
+ const walked = new Set([symbol.id]);
77
+ let at = stepOf(symbol);
78
+ while (at !== undefined && !walked.has(at.id)) {
79
+ walked.add(at.id);
80
+ const ids = declarationsOf(at);
81
+ if (ids.length > 0) {
82
+ return ids;
83
+ }
84
+ at = stepOf(at);
85
+ }
86
+ return [];
87
+ };
88
+ return {
89
+ declarationsOf,
90
+ chainOf,
91
+ nextDeclared,
92
+ get steps() {
93
+ return asked;
94
+ },
95
+ };
96
+ }
@@ -8,15 +8,15 @@
8
8
  * project's checker, and that holds only for views the session itself hands out.
9
9
  */
10
10
  import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.js";
11
- import { graphOf } from "./graph.js";
11
+ import { computeExemptions, disabledClasses, exemptionsOf, retainedIn, } from "./exempt.js";
12
12
  import { inventory } from "./inventory.js";
13
13
  import { readManifest } from "./manifest.js";
14
+ import { matrixOf, sweepMatrix } from "./matrix.js";
14
15
  import { relativePath, resolvePath } from "./paths.js";
15
16
  import { byPosition } from "./position.js";
16
17
  import { references } from "./references.js";
17
18
  import { roots, unmatchedEverywhere, unmatchedRoots } from "./roots.js";
18
19
  import { diagnosticsOf, runSession } from "./session.js";
19
- import { sweep } from "./sweep.js";
20
20
  /** The setting whose source decides where a finding about a configured root sits. */
21
21
  const ROOTS_SETTING = "roots.patterns";
22
22
  /**
@@ -66,9 +66,7 @@ function readProjects(engine, host, scope, config, stage) {
66
66
  }
67
67
  const held = inventory(project, host, targetRoot);
68
68
  const rooted = roots(project, held, targetRoot, options);
69
- const configuration = ids.get(project.configFile) ??
70
- relativePath(targetRoot, project.configFile) ??
71
- project.configFile;
69
+ const configuration = ids.get(project.configFile) ?? project.configFile;
72
70
  return stage(project, { configuration, held, rooted });
73
71
  });
74
72
  if (failures.length > 0) {
@@ -120,23 +118,40 @@ export function runRoots(engine, host, scope, config, provenance) {
120
118
  findings: unmatchedRoots(unmatched, documentOf(host, provenance, ROOTS_SETTING, targetRoot)),
121
119
  };
122
120
  }
121
+ /** The exemption classes this analyzer detects, each by its detector; any other retains nothing. */
122
+ const DETECTORS = new Map();
123
123
  /**
124
- * Sweeps every project of the run the scope and the configuration describe, one
125
- * configuration at a time, in discovery order. Each project's references are read
126
- * against its own inventory with the configured test-file patterns, and its graph is
127
- * swept under the input the caller decided once for the run.
124
+ * Sweeps the run the scope and the configuration describe. Each project's references
125
+ * and exemptions are read against its own inventory while its view is open; the
126
+ * projects then merge into one matrix in the run's order, which is swept under the
127
+ * caller's marks and mode with the exemptions every project found. A record found in
128
+ * one configuration holds in every configuration that holds its declaration, because
129
+ * an exemption is evidence of a use the analysis cannot see and a use in one
130
+ * configuration is a use. A project that carries an error ends the run before any
131
+ * project is swept.
128
132
  */
129
- export function runSweeps(engine, host, scope, config, input) {
133
+ export function runSweep(engine, host, scope, config, input, detectors = DETECTORS) {
130
134
  const targetRoot = scope.target.path;
131
- return readProjects(engine, host, scope, config, (project, read) => {
132
- const resolved = references(project, read.held, targetRoot, {
135
+ const disabled = disabledClasses(config);
136
+ const read = readProjects(engine, host, scope, config, (project, projectRead) => {
137
+ const resolved = references(project, projectRead.held, targetRoot, {
133
138
  testFiles: config.ts.testFiles,
134
139
  });
135
- const graph = graphOf(read.held.symbols, resolved.references, read.rooted.liveUnderReachability, resolved.testFilePaths);
136
- return {
137
- configuration: read.configuration,
138
- symbols: read.held.symbols,
139
- sweep: sweep(graph, input),
140
+ const configured = {
141
+ configuration: projectRead.configuration,
142
+ symbols: projectRead.held.symbols,
143
+ references: resolved.references,
144
+ roots: projectRead.rooted.liveUnderReachability,
145
+ testFiles: resolved.testFilePaths,
140
146
  };
147
+ const exempt = computeExemptions({ project, held: projectRead.held, targetRoot }, detectors, {
148
+ disabled,
149
+ mode: input.mode,
150
+ testFiles: new Set(resolved.testFilePaths),
151
+ });
152
+ return { configured, exempt };
141
153
  });
154
+ const matrix = matrixOf(read.map((one) => one.configured));
155
+ const swept = { ...input, exempt: exemptionsOf(read.flatMap((one) => one.exempt)) };
156
+ return { matrix, sweep: sweepMatrix(matrix, swept), retained: retainedIn(matrix, swept) };
142
157
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The exemption classes of the Contract's vocabulary, in its order: each class's name,
3
+ * the languages it runs on and the confidence it is recorded at.
4
+ *
5
+ * The rows are written out rather than read from the Contract at run time, for the
6
+ * reason the fixed severity codes are, and a test holds them equal to the Contract
7
+ * release this analyzer implements. A class name is a type, so a detector table or a
8
+ * record naming a class the vocabulary does not hold, or one that runs on another
9
+ * language only, does not compile.
10
+ */
11
+ /** Every class of the vocabulary, in the order the Contract lists them. */
12
+ export const EXEMPTION_CLASSES = [
13
+ { class: "interface-satisfaction", languages: ["go", "ts"], confidence: "certain" },
14
+ { class: "encoding-reflection", languages: ["go"], confidence: "certain" },
15
+ { class: "format-verb-contract", languages: ["go"], confidence: "certain" },
16
+ { class: "errors-duck-typing", languages: ["go"], confidence: "certain" },
17
+ { class: "enum-group", languages: ["go", "ts"], confidence: "certain" },
18
+ { class: "generated-file", languages: ["go"], confidence: "certain" },
19
+ { class: "linkname-cgo-asm-plugin", languages: ["go"], confidence: "certain" },
20
+ { class: "template-field", languages: ["go", "ts"], confidence: "possible" },
21
+ { class: "reflective-lookup", languages: ["go", "ts"], confidence: "possible" },
22
+ { class: "decorator", languages: ["ts"], confidence: "certain" },
23
+ { class: "injection-container", languages: ["ts"], confidence: "certain" },
24
+ { class: "framework-lifecycle", languages: ["ts"], confidence: "certain" },
25
+ { class: "serialization-contract", languages: ["ts"], confidence: "certain" },
26
+ ];
27
+ function runsOnTS(row) {
28
+ const languages = row.languages;
29
+ return languages.includes("ts");
30
+ }
31
+ /** The classes that run on TypeScript, in the vocabulary's order. */
32
+ export const TS_EXEMPTION_CLASSES = EXEMPTION_CLASSES.filter(runsOnTS).map((row) => row.class);
33
+ /** Whether one name is a class of the vocabulary, whichever language it runs on. */
34
+ export function isExemptionClass(name) {
35
+ return EXEMPTION_CLASSES.some((row) => row.class === name);
36
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The exemption framework: the classes of reason for which a declaration the reference
3
+ * graph alone would report is live all the same. A class is policy over what one
4
+ * project's checker answers, and its detector names the declarations it holds back;
5
+ * this module runs the detectors, decides which of their records stand, and answers
6
+ * which records held something back. It names no class's detector: the composition
7
+ * hands it the table of the classes it has.
8
+ */
9
+ import { ConfigError } from "./config.js";
10
+ import { EXEMPTION_CLASSES, isExemptionClass, TS_EXEMPTION_CLASSES, } from "./exempt-classes.js";
11
+ import { byPosition, positionKey } from "./position.js";
12
+ import { sweep } from "./sweep.js";
13
+ /**
14
+ * The classes `exemptions.disabled` switches off. A name is refused unless the
15
+ * vocabulary holds it; a class that runs on another language only is admitted and
16
+ * switches nothing off here, so one configuration serves every analyzer.
17
+ */
18
+ export function disabledClasses(config) {
19
+ const disabled = new Set();
20
+ for (const name of config.exemptionsDisabled) {
21
+ if (!isExemptionClass(name)) {
22
+ const known = EXEMPTION_CLASSES.map((row) => row.class).join(", ");
23
+ throw new ConfigError("malformed", "exemptions.disabled", `exemptions.disabled: ${JSON.stringify(name)} is not an exemption class: the classes are ${known}`);
24
+ }
25
+ disabled.add(name);
26
+ }
27
+ return disabled;
28
+ }
29
+ /**
30
+ * The records one project's detectors found, each class in the vocabulary's order and
31
+ * none of a switched-off class. Under a production mode a record whose evidence is
32
+ * written in a test file holds nothing, as a reference a test file makes is none there.
33
+ */
34
+ export function computeExemptions(input, detectors, holding) {
35
+ const found = [];
36
+ for (const exemptionClass of TS_EXEMPTION_CLASSES) {
37
+ const detect = detectors.get(exemptionClass);
38
+ if (detect === undefined || holding.disabled.has(exemptionClass)) {
39
+ continue;
40
+ }
41
+ for (const evidence of detect(input)) {
42
+ if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
43
+ continue;
44
+ }
45
+ found.push({
46
+ id: evidence.id,
47
+ class: exemptionClass,
48
+ detail: evidence.detail,
49
+ site: evidence.site,
50
+ });
51
+ }
52
+ }
53
+ return exemptionsOf(found);
54
+ }
55
+ /** Two strings ordered bytewise. */
56
+ function compare(a, b) {
57
+ if (a === b) {
58
+ return 0;
59
+ }
60
+ return a < b ? -1 : 1;
61
+ }
62
+ /**
63
+ * Records as a run holds them: ordered by site, then class, then declaration, and one
64
+ * per declaration, class and detail, the one at the first site. A fact found at many
65
+ * sites, or by several projects, is one record.
66
+ */
67
+ export function exemptionsOf(found) {
68
+ const ordered = [...found].sort((a, b) => byPosition(a.site, b.site) || compare(a.class, b.class) || compare(a.id, b.id));
69
+ const seen = new Set();
70
+ return ordered.filter((record) => {
71
+ const fact = [record.id, record.class, record.detail].join("\u0000");
72
+ if (seen.has(fact)) {
73
+ return false;
74
+ }
75
+ seen.add(fact);
76
+ return true;
77
+ });
78
+ }
79
+ /** The records per declaration, each declaration's in the order given. */
80
+ function byDeclaration(records) {
81
+ const grouped = new Map();
82
+ for (const record of records) {
83
+ const held = grouped.get(record.id);
84
+ if (held === undefined) {
85
+ grouped.set(record.id, [record]);
86
+ }
87
+ else {
88
+ held.push(record);
89
+ }
90
+ }
91
+ return grouped;
92
+ }
93
+ /**
94
+ * The records that held back a declaration some configuration of the run would
95
+ * otherwise judge dead: each configuration's graph is swept once more with no
96
+ * exemption and the marks it was given, and a record is retained when one of those
97
+ * sweeps judged its declaration a candidate. A record that holds a declaration back in
98
+ * one configuration is retained whatever another configuration answers about it, as a
99
+ * run of that configuration alone would report the declaration without it. A record
100
+ * on a declaration that a relation or a mark holds live in every configuration held
101
+ * nothing back. The order is the declarations' site order, and for one declaration
102
+ * the input's.
103
+ */
104
+ export function retainedIn(matrix, input) {
105
+ const exempt = input.exempt ?? [];
106
+ if (exempt.length === 0) {
107
+ return [];
108
+ }
109
+ const bare = { marked: input.marked, mode: input.mode };
110
+ const dead = new Set(matrix.graphs.flatMap((graph) => sweep(graph, bare).candidates.map((candidate) => candidate.id)));
111
+ const held = byDeclaration(exempt.filter((record) => dead.has(record.id)));
112
+ return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
113
+ }
114
+ /**
115
+ * One line per held-back declaration, in site order: its reference, then each record's
116
+ * class, site and detail, all separated by tabs.
117
+ */
118
+ export function retainedLines(symbols, retained) {
119
+ const held = byDeclaration(retained);
120
+ return symbols.flatMap((symbol) => {
121
+ const records = held.get(symbol.id);
122
+ if (records === undefined) {
123
+ return [];
124
+ }
125
+ const fields = records.flatMap((record) => [
126
+ record.class,
127
+ positionKey(record.site),
128
+ record.detail,
129
+ ]);
130
+ return [[symbol.ref, ...fields].join("\t")];
131
+ });
132
+ }
package/dist/src/graph.js CHANGED
@@ -85,7 +85,11 @@ export function graphOf(symbols, references, roots, testFiles) {
85
85
  counts.production += 1;
86
86
  }
87
87
  }
88
- out[at(reference.from)]?.push({ to, test: reference.test });
88
+ out[at(reference.from)]?.push({
89
+ to,
90
+ test: reference.test,
91
+ evaluation: reference.use === "evaluation",
92
+ });
89
93
  }
90
94
  const parent = symbols.map((symbol) => at(symbol.parent));
91
95
  const exportsOf = symbols.map(() => []);
@@ -0,0 +1,138 @@
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 } from "./graph.js";
8
+ import { byPosition } from "./position.js";
9
+ import { sweep } from "./sweep.js";
10
+ /** Two strings ordered bytewise. */
11
+ function compare(a, b) {
12
+ if (a === b) {
13
+ return 0;
14
+ }
15
+ return a < b ? -1 : 1;
16
+ }
17
+ /** What makes one reference one reference, whichever configuration made it. */
18
+ function referenceKey(reference) {
19
+ const { path, line, column } = reference.position;
20
+ return [reference.from, reference.to, path, String(line), String(column), reference.use].join("\u0000");
21
+ }
22
+ /**
23
+ * Indexes what the passes answered for each configuration of one run, in the run's order.
24
+ *
25
+ * A declaration is keyed across configurations by its identifier, which is its rendered
26
+ * position and so is the same in every configuration that compiles its file; the fields
27
+ * of the first configuration that holds it are the ones kept. A declaration one
28
+ * configuration alone holds is in the union with that one configuration, so it is
29
+ * judged where it exists rather than skipped.
30
+ *
31
+ * One reference written at one position of a file several configurations compile is seen
32
+ * once per configuration and is one reference, so the union holds it once; two references
33
+ * one configuration makes at one position are two.
34
+ */
35
+ export function matrixOf(per) {
36
+ const symbols = [];
37
+ const holders = new Map();
38
+ const references = [];
39
+ const firstSeen = new Map();
40
+ per.forEach((one, index) => {
41
+ for (const symbol of one.symbols) {
42
+ const held = holders.get(symbol.id);
43
+ if (held === undefined) {
44
+ holders.set(symbol.id, [one.configuration]);
45
+ symbols.push(symbol);
46
+ }
47
+ else if (!held.includes(one.configuration)) {
48
+ held.push(one.configuration);
49
+ }
50
+ }
51
+ for (const reference of one.references) {
52
+ const key = referenceKey(reference);
53
+ const seen = firstSeen.get(key);
54
+ if (seen === undefined) {
55
+ firstSeen.set(key, index);
56
+ }
57
+ else if (seen !== index) {
58
+ continue;
59
+ }
60
+ references.push(reference);
61
+ }
62
+ });
63
+ symbols.sort((a, b) => byPosition(a.position, b.position) || compare(a.ref, b.ref));
64
+ return {
65
+ configurations: per.map((one) => one.configuration),
66
+ graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles)),
67
+ union: graphOf(symbols, references, per.flatMap((one) => one.roots), [...new Set(per.flatMap((one) => one.testFiles))]),
68
+ heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
69
+ };
70
+ }
71
+ /**
72
+ * Answers which declarations of the run are dead in every configuration that holds them.
73
+ *
74
+ * Liveness is a question about one configuration, because a file another configuration
75
+ * does not compile makes no reference there and a root it does not hold seeds nothing
76
+ * there, so each configuration is swept on its own graph under the run's input. A
77
+ * declaration is a candidate when every configuration holding it judged it one: a
78
+ * reference under one configuration is a reference, so what one project uses is never
79
+ * reported, and a configuration that does not hold a declaration says nothing about it.
80
+ * The candidate's relation is reference counting where every configuration found it so,
81
+ * and reachability otherwise; its counts are the union's, one per reference.
82
+ *
83
+ * The components are computed once, over the union and the intersected dead set, so one
84
+ * dead component is one entry of the run with one identifier.
85
+ */
86
+ export function sweepMatrix(matrix, input) {
87
+ const per = matrix.graphs.map((graph) => sweep(graph, input));
88
+ const byConfiguration = new Map(matrix.configurations.map((configuration, index) => [
89
+ configuration,
90
+ new Map((per[index]?.candidates ?? []).map((candidate) => [candidate.id, candidate])),
91
+ ]));
92
+ const union = matrix.union;
93
+ const dead = union.symbols.map(() => false);
94
+ const testOfDeadCode = union.symbols.map(() => false);
95
+ const candidates = [];
96
+ union.symbols.forEach((symbol, at) => {
97
+ const held = matrix.heldIn[at] ?? [];
98
+ const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
99
+ if (held.length === 0 || found.some((candidate) => candidate === undefined)) {
100
+ return;
101
+ }
102
+ const all = found.filter((candidate) => candidate !== undefined);
103
+ const admitted = all.every((candidate) => candidate.testOfDeadCode);
104
+ dead[at] = true;
105
+ testOfDeadCode[at] = admitted;
106
+ candidates.push({
107
+ id: symbol.id,
108
+ productionRefs: union.made[at]?.production ?? 0,
109
+ testRefs: union.made[at]?.test ?? 0,
110
+ relation: all.every((candidate) => candidate.relation === "reference-counting")
111
+ ? "reference-counting"
112
+ : "reachability",
113
+ testOfDeadCode: admitted,
114
+ configurations: held,
115
+ });
116
+ });
117
+ return {
118
+ liveUnder: liveAnywhere(union.symbols, per),
119
+ candidates,
120
+ components: componentsOf(union, dead, testOfDeadCode),
121
+ };
122
+ }
123
+ /** The relations in the order a declaration's entry lists them. */
124
+ const RELATIONS = ["reference-counting", "reachability"];
125
+ /**
126
+ * Per declaration, in site order, every relation that holds it live in at least one
127
+ * configuration.
128
+ */
129
+ function liveAnywhere(symbols, per) {
130
+ const live = new Map();
131
+ for (const symbol of symbols) {
132
+ const relations = RELATIONS.filter((relation) => per.some((liveness) => (liveness.liveUnder.get(symbol.id) ?? []).includes(relation)));
133
+ if (relations.length > 0) {
134
+ live.set(symbol.id, relations);
135
+ }
136
+ }
137
+ return live;
138
+ }