@cplieger/deadset-ts 2.3.0 → 3.1.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/src/graph.ts ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * One project's declarations, the references between them and the roots that seed
3
+ * reachability, indexed for the sweep. The graph reads no file and asks the compiler
4
+ * nothing, so a sweep over it is a function of the values it was built from, and the
5
+ * inventory's order is the order every set a sweep answers reads in.
6
+ */
7
+
8
+ import type { InventorySymbol, SymbolKind } from "./inventory.ts";
9
+ import type { Reference } from "./references.ts";
10
+ import type { Root, RootKind } from "./roots.ts";
11
+
12
+ /** The position of a declaration the inventory does not hold. */
13
+ export const OUTSIDE = -1;
14
+
15
+ /**
16
+ * Whether the sweep judges a declaration of each kind. A file is not judged by either
17
+ * relation: whether a file is built and whether anything imports it are the subjects of
18
+ * the file kinds, which read the program rather than ask whether anything references the
19
+ * file. The Contract's finding schema names the kinds that carry no liveness relation,
20
+ * and a test holds this table to that list.
21
+ */
22
+ const SWEPT: Readonly<Record<SymbolKind, boolean>> = {
23
+ file: false,
24
+ function: true,
25
+ class: true,
26
+ interface: true,
27
+ type: true,
28
+ enum: true,
29
+ namespace: true,
30
+ variable: true,
31
+ "export-alias": true,
32
+ method: true,
33
+ "class-member": true,
34
+ "interface-method": true,
35
+ "type-member": true,
36
+ "enum-member": true,
37
+ "type-parameter": true,
38
+ };
39
+
40
+ /**
41
+ * Whether a root of each kind names a caller, which holds it live under both relations: a
42
+ * tool reads the configuration it loads, a runtime runs a test file, a setup file and a
43
+ * worker, and a configured root asserts a caller. The other kinds only suppose a consumer
44
+ * that imports what a manifest entry, a binary or an entry file exports, or names a
45
+ * library's published API, so such a root is live under reachability alone.
46
+ */
47
+ const NAMES_A_CALLER: Readonly<Record<RootKind, boolean>> = {
48
+ "entry-file": false,
49
+ "manifest-entry": false,
50
+ "manifest-binary": false,
51
+ "published-api": false,
52
+ configured: true,
53
+ pattern: true,
54
+ "test-runner": true,
55
+ "lint-configuration": true,
56
+ "mutation-testing": true,
57
+ "browser-tests": true,
58
+ worker: true,
59
+ };
60
+
61
+ /** Whether the sweep judges a declaration of one kind. */
62
+ export function swept(kind: SymbolKind): boolean {
63
+ return SWEPT[kind];
64
+ }
65
+
66
+ /** Whether a root of one kind names a caller rather than supposing one. */
67
+ export function namesACaller(kind: RootKind): boolean {
68
+ return NAMES_A_CALLER[kind];
69
+ }
70
+
71
+ /** One reference as the sweep reads it: the declaration named, and whether a test file made it. */
72
+ export interface Edge {
73
+ readonly to: number;
74
+ readonly test: boolean;
75
+ /**
76
+ * Whether the reference evaluates the module whose file it names, which reaches the
77
+ * module's top level and none of what it exports.
78
+ */
79
+ readonly evaluation: boolean;
80
+ }
81
+
82
+ /** How many references one declaration carries, split by the classification of the file that made each. */
83
+ export interface Counts {
84
+ readonly production: number;
85
+ readonly test: number;
86
+ }
87
+
88
+ /** One root the inventory holds, at the position of the declaration it names. */
89
+ export interface Rooted {
90
+ readonly at: number;
91
+ readonly kind: RootKind;
92
+ }
93
+
94
+ /** The index the sweep and the component pass read. */
95
+ export interface Graph {
96
+ /** The declarations, in the order the inventory returns them. */
97
+ readonly symbols: readonly InventorySymbol[];
98
+ /** Per declaration, the references it makes to declarations the inventory holds. */
99
+ readonly out: readonly (readonly Edge[])[];
100
+ /** Per declaration, the references made to it, whoever made them. */
101
+ readonly made: readonly Counts[];
102
+ /** Per declaration, the position of its container, or {@link OUTSIDE} for a file. */
103
+ readonly parent: readonly number[];
104
+ /** Per declaration, whether a test file holds it. */
105
+ readonly test: readonly boolean[];
106
+ /** Per declaration, whether the sweep judges its liveness. */
107
+ readonly subject: readonly boolean[];
108
+ /**
109
+ * Per declaration, the declarations its export table names where it is a file, and
110
+ * nothing for any other declaration. A reference that reads a file reads the module's
111
+ * namespace, which reaches everything the module exports.
112
+ */
113
+ readonly exportsOf: readonly (readonly number[])[];
114
+ /** Every root naming a declaration the inventory holds, in the order the root set gives them. */
115
+ readonly rooted: readonly Rooted[];
116
+ /** The position of the declaration one identifier names, or {@link OUTSIDE}. */
117
+ at(id: string): number;
118
+ }
119
+
120
+ /**
121
+ * Indexes one project's inventory, references and roots. `testFiles` are the paths the
122
+ * reference pass classified as test files, so a declaration one holds is a test
123
+ * declaration. A reference to a declaration the inventory does not hold is dropped; one
124
+ * from such a declaration still counts toward its target, because reference counting
125
+ * asks whether any declaration names the target. A root naming nothing is dropped.
126
+ */
127
+ export function graphOf(
128
+ symbols: readonly InventorySymbol[],
129
+ references: readonly Reference[],
130
+ roots: readonly Root[],
131
+ testFiles: readonly string[],
132
+ ): Graph {
133
+ const index = new Map(symbols.map((symbol, at) => [symbol.id, at]));
134
+ const at = (id: string): number => index.get(id) ?? OUTSIDE;
135
+ const tests = new Set(testFiles);
136
+
137
+ const out: Edge[][] = symbols.map(() => []);
138
+ const made = symbols.map(() => ({ production: 0, test: 0 }));
139
+ for (const reference of references) {
140
+ const to = at(reference.to);
141
+ if (to === OUTSIDE) {
142
+ continue;
143
+ }
144
+ const counts = made[to];
145
+ if (counts !== undefined) {
146
+ if (reference.test) {
147
+ counts.test += 1;
148
+ } else {
149
+ counts.production += 1;
150
+ }
151
+ }
152
+ out[at(reference.from)]?.push({
153
+ to,
154
+ test: reference.test,
155
+ evaluation: reference.use === "evaluation",
156
+ });
157
+ }
158
+
159
+ const parent = symbols.map((symbol) => at(symbol.parent));
160
+ const exportsOf: number[][] = symbols.map(() => []);
161
+ symbols.forEach((symbol, position) => {
162
+ const container = parent[position] ?? OUTSIDE;
163
+ if (symbol.exported && symbols[container]?.kind === "file") {
164
+ exportsOf[container]?.push(position);
165
+ }
166
+ });
167
+
168
+ const rooted: Rooted[] = [];
169
+ for (const root of roots) {
170
+ const position = at(root.id);
171
+ if (position !== OUTSIDE) {
172
+ rooted.push({ at: position, kind: root.kind });
173
+ }
174
+ }
175
+
176
+ return {
177
+ symbols,
178
+ out,
179
+ made,
180
+ parent,
181
+ test: symbols.map((symbol) => tests.has(symbol.position.path)),
182
+ subject: symbols.map((symbol) => swept(symbol.kind)),
183
+ exportsOf,
184
+ rooted,
185
+ at,
186
+ };
187
+ }
package/src/index.ts CHANGED
@@ -1,26 +1,6 @@
1
1
  export { run, SETTING_OPTIONS, type Writer } from "./run.ts";
2
2
  export type { DirectoryEntry, Host, PathKind } from "./host.ts";
3
3
  export { DECLINED_CONVENTIONS, type DeclinedConvention } from "./entry-point-gaps.ts";
4
- export {
5
- inventory,
6
- nodeKey,
7
- type Inventory,
8
- type InventoryCost,
9
- type InventorySymbol,
10
- type SymbolKind,
11
- type Visibility,
12
- } from "./inventory.ts";
13
- export {
14
- DEFAULT_BATCH_CAP,
15
- references,
16
- type Reference,
17
- type ReferenceCost,
18
- type ReferenceOptions,
19
- type References,
20
- type Resolution,
21
- type TestFileRule,
22
- type Use,
23
- } from "./references.ts";
24
4
  export {
25
5
  byPosition,
26
6
  positionKey,
package/src/matrix.ts ADDED
@@ -0,0 +1,212 @@
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
+
7
+ import { componentsOf, type Component } from "./components.ts";
8
+ import { graphOf, type Graph } from "./graph.ts";
9
+ import type { InventorySymbol } from "./inventory.ts";
10
+ import { byPosition } from "./position.ts";
11
+ import type { Reference } from "./references.ts";
12
+ import type { Root } from "./roots.ts";
13
+ import { sweep, type Candidate, type Liveness, type Relation, type SweepInput } from "./sweep.ts";
14
+
15
+ /** What the passes answered for one configuration of the run. */
16
+ export interface Configured {
17
+ /** The name discovery gives the configuration's project. */
18
+ readonly configuration: string;
19
+ readonly symbols: readonly InventorySymbol[];
20
+ readonly references: readonly Reference[];
21
+ readonly roots: readonly Root[];
22
+ /** The paths the reference pass classified as test files. */
23
+ readonly testFiles: readonly string[];
24
+ }
25
+
26
+ /** The run's configurations, indexed for the sweep. */
27
+ export interface Matrix {
28
+ /** The configurations, in the run's order. */
29
+ readonly configurations: readonly string[];
30
+ /** Per configuration, in the same order, the graph of what that configuration holds. */
31
+ readonly graphs: readonly Graph[];
32
+ /**
33
+ * The graph over every configuration at once: every declaration any configuration
34
+ * holds, once per position and in site order, and every reference any configuration
35
+ * makes, a reference several configurations make at one position counted once.
36
+ */
37
+ readonly union: Graph;
38
+ /**
39
+ * Per declaration of {@link Matrix.union}, at its position there, the configurations
40
+ * whose project holds it, in the run's order.
41
+ */
42
+ readonly heldIn: readonly (readonly string[])[];
43
+ }
44
+
45
+ /** One declaration dead in every configuration that holds it. */
46
+ export interface MatrixCandidate extends Candidate {
47
+ /** The configurations the declaration is dead in, which are every one that holds it. */
48
+ readonly configurations: readonly string[];
49
+ }
50
+
51
+ /** One run's sweep. */
52
+ export interface SweepResult {
53
+ /**
54
+ * Per declaration, the relations that hold it live in at least one configuration,
55
+ * reference counting first. A declaration no relation holds live anywhere has no entry.
56
+ */
57
+ readonly liveUnder: ReadonlyMap<string, readonly Relation[]>;
58
+ /** The declarations dead in every configuration that holds them, in site order. */
59
+ readonly candidates: readonly MatrixCandidate[];
60
+ /** The dead components, each before every component it reaches, each identifier once per run. */
61
+ readonly components: readonly Component[];
62
+ }
63
+
64
+ /** Two strings ordered bytewise. */
65
+ function compare(a: string, b: string): number {
66
+ if (a === b) {
67
+ return 0;
68
+ }
69
+ return a < b ? -1 : 1;
70
+ }
71
+
72
+ /** What makes one reference one reference, whichever configuration made it. */
73
+ function referenceKey(reference: Reference): string {
74
+ const { path, line, column } = reference.position;
75
+ return [reference.from, reference.to, path, String(line), String(column), reference.use].join(
76
+ "\u0000",
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Indexes what the passes answered for each configuration of one run, in the run's order.
82
+ *
83
+ * A declaration is keyed across configurations by its identifier, which is its rendered
84
+ * position and so is the same in every configuration that compiles its file; the fields
85
+ * of the first configuration that holds it are the ones kept. A declaration one
86
+ * configuration alone holds is in the union with that one configuration, so it is
87
+ * judged where it exists rather than skipped.
88
+ *
89
+ * One reference written at one position of a file several configurations compile is seen
90
+ * once per configuration and is one reference, so the union holds it once; two references
91
+ * one configuration makes at one position are two.
92
+ */
93
+ export function matrixOf(per: readonly Configured[]): Matrix {
94
+ const symbols: InventorySymbol[] = [];
95
+ const holders = new Map<string, string[]>();
96
+ const references: Reference[] = [];
97
+ const firstSeen = new Map<string, number>();
98
+ per.forEach((one, index) => {
99
+ for (const symbol of one.symbols) {
100
+ const held = holders.get(symbol.id);
101
+ if (held === undefined) {
102
+ holders.set(symbol.id, [one.configuration]);
103
+ symbols.push(symbol);
104
+ } else if (!held.includes(one.configuration)) {
105
+ held.push(one.configuration);
106
+ }
107
+ }
108
+ for (const reference of one.references) {
109
+ const key = referenceKey(reference);
110
+ const seen = firstSeen.get(key);
111
+ if (seen === undefined) {
112
+ firstSeen.set(key, index);
113
+ } else if (seen !== index) {
114
+ continue;
115
+ }
116
+ references.push(reference);
117
+ }
118
+ });
119
+ symbols.sort((a, b) => byPosition(a.position, b.position) || compare(a.ref, b.ref));
120
+
121
+ return {
122
+ configurations: per.map((one) => one.configuration),
123
+ graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles)),
124
+ union: graphOf(
125
+ symbols,
126
+ references,
127
+ per.flatMap((one) => one.roots),
128
+ [...new Set(per.flatMap((one) => one.testFiles))],
129
+ ),
130
+ heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Answers which declarations of the run are dead in every configuration that holds them.
136
+ *
137
+ * Liveness is a question about one configuration, because a file another configuration
138
+ * does not compile makes no reference there and a root it does not hold seeds nothing
139
+ * there, so each configuration is swept on its own graph under the run's input. A
140
+ * declaration is a candidate when every configuration holding it judged it one: a
141
+ * reference under one configuration is a reference, so what one project uses is never
142
+ * reported, and a configuration that does not hold a declaration says nothing about it.
143
+ * The candidate's relation is reference counting where every configuration found it so,
144
+ * and reachability otherwise; its counts are the union's, one per reference.
145
+ *
146
+ * The components are computed once, over the union and the intersected dead set, so one
147
+ * dead component is one entry of the run with one identifier.
148
+ */
149
+ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
150
+ const per = matrix.graphs.map((graph) => sweep(graph, input));
151
+ const byConfiguration = new Map<string, ReadonlyMap<string, Candidate>>(
152
+ matrix.configurations.map((configuration, index) => [
153
+ configuration,
154
+ new Map((per[index]?.candidates ?? []).map((candidate) => [candidate.id, candidate])),
155
+ ]),
156
+ );
157
+
158
+ const union = matrix.union;
159
+ const dead = union.symbols.map(() => false);
160
+ const testOfDeadCode = union.symbols.map(() => false);
161
+ const candidates: MatrixCandidate[] = [];
162
+ union.symbols.forEach((symbol, at) => {
163
+ const held = matrix.heldIn[at] ?? [];
164
+ const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
165
+ if (held.length === 0 || found.some((candidate) => candidate === undefined)) {
166
+ return;
167
+ }
168
+ const all = found.filter((candidate): candidate is Candidate => candidate !== undefined);
169
+ const admitted = all.every((candidate) => candidate.testOfDeadCode);
170
+ dead[at] = true;
171
+ testOfDeadCode[at] = admitted;
172
+ candidates.push({
173
+ id: symbol.id,
174
+ productionRefs: union.made[at]?.production ?? 0,
175
+ testRefs: union.made[at]?.test ?? 0,
176
+ relation: all.every((candidate) => candidate.relation === "reference-counting")
177
+ ? "reference-counting"
178
+ : "reachability",
179
+ testOfDeadCode: admitted,
180
+ configurations: held,
181
+ });
182
+ });
183
+
184
+ return {
185
+ liveUnder: liveAnywhere(union.symbols, per),
186
+ candidates,
187
+ components: componentsOf(union, dead, testOfDeadCode),
188
+ };
189
+ }
190
+
191
+ /** The relations in the order a declaration's entry lists them. */
192
+ const RELATIONS: readonly Relation[] = ["reference-counting", "reachability"];
193
+
194
+ /**
195
+ * Per declaration, in site order, every relation that holds it live in at least one
196
+ * configuration.
197
+ */
198
+ function liveAnywhere(
199
+ symbols: readonly InventorySymbol[],
200
+ per: readonly Liveness[],
201
+ ): ReadonlyMap<string, readonly Relation[]> {
202
+ const live = new Map<string, readonly Relation[]>();
203
+ for (const symbol of symbols) {
204
+ const relations = RELATIONS.filter((relation) =>
205
+ per.some((liveness) => (liveness.liveUnder.get(symbol.id) ?? []).includes(relation)),
206
+ );
207
+ if (relations.length > 0) {
208
+ live.set(symbol.id, relations);
209
+ }
210
+ }
211
+ return live;
212
+ }
package/src/print.ts CHANGED
@@ -20,12 +20,11 @@ export function printConfig(config: Config, provenance: Provenance): string {
20
20
  min_confidence: config.analysis.minConfidence,
21
21
  generated_files: config.analysis.generatedFiles,
22
22
  consumer_tests: config.analysis.consumerTests,
23
- configurations: config.analysis.configurations.map((entry) => ({
24
- id: entry.id,
25
- os: entry.os,
26
- arch: entry.arch,
27
- tags: entry.tags,
28
- })),
23
+ configurations: config.analysis.configurations.map((entry) =>
24
+ entry.shape === "project"
25
+ ? { id: entry.id, project: entry.project }
26
+ : { id: entry.id, os: entry.os, arch: entry.arch, tags: entry.tags },
27
+ ),
29
28
  matrix: { complete: config.analysis.matrixComplete },
30
29
  template_dirs: config.analysis.templateDirs,
31
30
  template_delimiters: {