@cplieger/deadset-ts 2.3.0 → 3.0.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 +7 -16
- package/dist/src/analysis.js +49 -16
- package/dist/src/components.js +289 -0
- package/dist/src/discover.js +98 -24
- package/dist/src/graph.js +116 -0
- package/dist/src/index.js +0 -2
- package/dist/src/print.js +3 -6
- package/dist/src/references.js +154 -49
- package/dist/src/resolve.js +82 -20
- package/dist/src/run.js +11 -7
- package/dist/src/schema.js +1 -1
- package/dist/src/sweep.js +156 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +91 -24
- package/src/components.ts +372 -0
- package/src/config.ts +23 -2
- package/src/discover.ts +144 -29
- package/src/graph.ts +178 -0
- package/src/index.ts +0 -20
- package/src/print.ts +5 -6
- package/src/references.ts +199 -52
- package/src/resolve.ts +117 -24
- package/src/run.ts +11 -7
- package/src/schema.ts +5 -2
- package/src/sweep.ts +230 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which declarations of one graph are dead, under which liveness relation, and which
|
|
3
|
+
* dead component each belongs to. Reference counting holds a declaration live when any
|
|
4
|
+
* declaration references it, whatever that one's own liveness, and no root plays a part;
|
|
5
|
+
* reachability holds it live when a path of references reaches it from a root. So a
|
|
6
|
+
* declaration only unreachable ones reference is live under the first, dead under the second.
|
|
7
|
+
*/
|
|
8
|
+
import { componentsOf } from "./components.js";
|
|
9
|
+
import { namesACaller, OUTSIDE } from "./graph.js";
|
|
10
|
+
/**
|
|
11
|
+
* Answers which declarations of one graph are dead under the input's mode.
|
|
12
|
+
*
|
|
13
|
+
* The order is the order the answers depend on: the callers and the marks are live
|
|
14
|
+
* before either relation runs, the candidate set is every judged declaration at least
|
|
15
|
+
* one relation does not hold live, the tests of dead code join it, and the components
|
|
16
|
+
* are computed over the set that results.
|
|
17
|
+
*/
|
|
18
|
+
export function sweep(graph, input) {
|
|
19
|
+
const marked = graph.symbols.map(() => false);
|
|
20
|
+
for (const id of input.marked) {
|
|
21
|
+
const at = graph.at(id);
|
|
22
|
+
if (at !== OUTSIDE) {
|
|
23
|
+
marked[at] = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Every root and every mark seeds reachability. A root of a kind that names a caller,
|
|
27
|
+
// and a mark, are live under reference counting as well; a root that only supposes a
|
|
28
|
+
// caller is not.
|
|
29
|
+
const called = graph.symbols.map(() => false);
|
|
30
|
+
for (const root of graph.rooted) {
|
|
31
|
+
if (namesACaller(root.kind)) {
|
|
32
|
+
called[root.at] = true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const counted = (at) => {
|
|
36
|
+
const made = graph.made[at];
|
|
37
|
+
if (made === undefined) {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
return input.mode.production ? made.production : made.production + made.test;
|
|
41
|
+
};
|
|
42
|
+
const reached = reachable(graph, marked, input.mode);
|
|
43
|
+
const liveUnder = graph.symbols.map((_symbol, at) => {
|
|
44
|
+
const relations = [];
|
|
45
|
+
if (marked[at] === true || called[at] === true || counted(at) > 0) {
|
|
46
|
+
relations.push("reference-counting");
|
|
47
|
+
}
|
|
48
|
+
if (reached[at] === true) {
|
|
49
|
+
relations.push("reachability");
|
|
50
|
+
}
|
|
51
|
+
return relations;
|
|
52
|
+
});
|
|
53
|
+
const dead = graph.symbols.map((_symbol, at) => graph.subject[at] === true && (liveUnder[at]?.length ?? 0) < 2);
|
|
54
|
+
const testOfDeadCode = admitTestsOfDeadCode(graph, dead, marked);
|
|
55
|
+
const candidates = [];
|
|
56
|
+
const live = new Map();
|
|
57
|
+
graph.symbols.forEach((symbol, at) => {
|
|
58
|
+
const relations = liveUnder[at] ?? [];
|
|
59
|
+
if (relations.length > 0) {
|
|
60
|
+
live.set(symbol.id, relations);
|
|
61
|
+
}
|
|
62
|
+
if (dead[at] !== true) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
candidates.push({
|
|
66
|
+
id: symbol.id,
|
|
67
|
+
productionRefs: graph.made[at]?.production ?? 0,
|
|
68
|
+
testRefs: graph.made[at]?.test ?? 0,
|
|
69
|
+
relation: counted(at) === 0 ? "reference-counting" : "reachability",
|
|
70
|
+
testOfDeadCode: testOfDeadCode[at] === true,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
liveUnder: live,
|
|
75
|
+
candidates,
|
|
76
|
+
components: componentsOf(graph, dead, testOfDeadCode),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Per declaration, whether a seed reaches it. The two seeds reach through different
|
|
81
|
+
* reference sets: a marked declaration is live by a mechanism the analysis cannot see, so
|
|
82
|
+
* no mode withholds a reference it makes, while a root reaches through the references the
|
|
83
|
+
* mode counts. The marks run first, so what they reach is expanded under the wider rule.
|
|
84
|
+
*/
|
|
85
|
+
function reachable(graph, marked, mode) {
|
|
86
|
+
const reached = graph.symbols.map(() => false);
|
|
87
|
+
const expanded = graph.symbols.map(() => false);
|
|
88
|
+
const queue = [];
|
|
89
|
+
// A reference to a file is a reference to the module's namespace, which reaches what
|
|
90
|
+
// the module exports as well as its top level. A root does not expand: a file a
|
|
91
|
+
// runtime runs without reading its exports is a root by itself.
|
|
92
|
+
const enter = (at, viaReference) => {
|
|
93
|
+
if (!reached[at]) {
|
|
94
|
+
reached[at] = true;
|
|
95
|
+
queue.push(at);
|
|
96
|
+
}
|
|
97
|
+
if (!viaReference || expanded[at] === true) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
expanded[at] = true;
|
|
101
|
+
for (const exported of graph.exportsOf[at] ?? []) {
|
|
102
|
+
enter(exported, false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const walk = (held) => {
|
|
106
|
+
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
107
|
+
for (const edge of graph.out[at] ?? []) {
|
|
108
|
+
if (!held && mode.production && edge.test) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
enter(edge.to, true);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
marked.forEach((mark, at) => {
|
|
116
|
+
if (mark) {
|
|
117
|
+
enter(at, false);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
walk(true);
|
|
121
|
+
for (const root of graph.rooted) {
|
|
122
|
+
enter(root.at, false);
|
|
123
|
+
}
|
|
124
|
+
walk(false);
|
|
125
|
+
return reached;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Admits every test declaration whose set of referenced production declarations is
|
|
129
|
+
* non-empty and wholly dead, and returns which it admitted. The set is read from every
|
|
130
|
+
* reference the test makes, not the ones the mode counts: a production sweep counts none,
|
|
131
|
+
* which is what makes the targets dead. A test referencing one live target is never admitted.
|
|
132
|
+
*/
|
|
133
|
+
function admitTestsOfDeadCode(graph, dead, marked) {
|
|
134
|
+
const admitted = graph.symbols.map(() => false);
|
|
135
|
+
graph.symbols.forEach((_symbol, at) => {
|
|
136
|
+
if (graph.test[at] !== true || graph.subject[at] !== true || marked[at] === true) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
let targets = 0;
|
|
140
|
+
let live = 0;
|
|
141
|
+
for (const edge of graph.out[at] ?? []) {
|
|
142
|
+
if (graph.test[edge.to] === true || graph.subject[edge.to] !== true) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
targets += 1;
|
|
146
|
+
if (dead[edge.to] !== true) {
|
|
147
|
+
live += 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (targets > 0 && live === 0) {
|
|
151
|
+
dead[at] = true;
|
|
152
|
+
admitted[at] = true;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return admitted;
|
|
156
|
+
}
|
package/dist/src/version.js
CHANGED
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/analysis.ts
CHANGED
|
@@ -12,14 +12,17 @@ import type { Diagnostic } from "@typescript/native/unstable/sync";
|
|
|
12
12
|
import type { Config, Provenance } from "./config.ts";
|
|
13
13
|
import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.ts";
|
|
14
14
|
import type { Finding } from "./finding.ts";
|
|
15
|
+
import { graphOf } from "./graph.ts";
|
|
15
16
|
import type { Host } from "./host.ts";
|
|
16
|
-
import { inventory } from "./inventory.ts";
|
|
17
|
+
import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
|
|
17
18
|
import { readManifest } from "./manifest.ts";
|
|
18
19
|
import { relativePath, resolvePath } from "./paths.ts";
|
|
19
20
|
import { byPosition, type Position } from "./position.ts";
|
|
20
|
-
import {
|
|
21
|
+
import { references } from "./references.ts";
|
|
22
|
+
import { roots, unmatchedEverywhere, unmatchedRoots, type RootKind, type Roots } from "./roots.ts";
|
|
21
23
|
import type { Scope } from "./scope.ts";
|
|
22
|
-
import { diagnosticsOf, runSession, type Engine } from "./session.ts";
|
|
24
|
+
import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
|
|
25
|
+
import { sweep, type SweepInput, type SweepResult } from "./sweep.ts";
|
|
23
26
|
|
|
24
27
|
/** The setting whose source decides where a finding about a configured root sits. */
|
|
25
28
|
const ROOTS_SETTING = "roots.patterns";
|
|
@@ -39,8 +42,8 @@ export interface RunRoot {
|
|
|
39
42
|
/** The root set of one run, and the findings about the roots the configuration names. */
|
|
40
43
|
export interface RunRoots {
|
|
41
44
|
/**
|
|
42
|
-
* The configurations the run analyzed
|
|
43
|
-
*
|
|
45
|
+
* The configurations the run analyzed, each by the name discovery gives its
|
|
46
|
+
* project, in discovery order.
|
|
44
47
|
*/
|
|
45
48
|
readonly configurations: readonly string[];
|
|
46
49
|
/**
|
|
@@ -80,26 +83,31 @@ function compare(a: string, b: string): number {
|
|
|
80
83
|
return a < b ? -1 : 1;
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
/** What every stage after the root set reads of one project. */
|
|
87
|
+
interface ProjectRead {
|
|
88
|
+
readonly configuration: string;
|
|
89
|
+
readonly held: Inventory;
|
|
90
|
+
readonly rooted: Roots;
|
|
91
|
+
}
|
|
92
|
+
|
|
83
93
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* configured string is a finding when it names nothing in every project, and the
|
|
89
|
-
* finding sits in the document that declared the roots.
|
|
94
|
+
* Reads every project the scope discovers, in one snapshot: each is checked for errors
|
|
95
|
+
* first, then enumerated, then rooted, and `stage` reads the rest of what it needs from
|
|
96
|
+
* the project while its view is open. A project carrying an error fails the run with no
|
|
97
|
+
* answer, as every verb that reads declarations does.
|
|
90
98
|
*/
|
|
91
|
-
|
|
99
|
+
function readProjects<Answer>(
|
|
92
100
|
engine: Engine,
|
|
93
101
|
host: Host,
|
|
94
102
|
scope: Scope,
|
|
95
103
|
config: Config,
|
|
96
|
-
|
|
97
|
-
):
|
|
104
|
+
stage: <Brand>(project: ProjectView<Brand>, read: ProjectRead) => Answer,
|
|
105
|
+
): readonly Answer[] {
|
|
98
106
|
const targetRoot = scope.target.path;
|
|
99
|
-
const discovered = discoverProjects(engine, host, scope);
|
|
100
|
-
const
|
|
107
|
+
const discovered = discoverProjects(engine, host, scope, config.analysis.configurations);
|
|
108
|
+
const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
|
|
101
109
|
const options = {
|
|
102
|
-
manifest,
|
|
110
|
+
manifest: readManifest(host, targetRoot),
|
|
103
111
|
patterns: config.rootPatterns,
|
|
104
112
|
entryFiles: config.ts.entryFiles,
|
|
105
113
|
testFiles: config.ts.testFiles,
|
|
@@ -113,7 +121,12 @@ export function runRoots(
|
|
|
113
121
|
return undefined;
|
|
114
122
|
}
|
|
115
123
|
const held = inventory(project, host, targetRoot);
|
|
116
|
-
|
|
124
|
+
const rooted = roots(project, held, targetRoot, options);
|
|
125
|
+
const configuration =
|
|
126
|
+
ids.get(project.configFile) ??
|
|
127
|
+
relativePath(targetRoot, project.configFile) ??
|
|
128
|
+
project.configFile;
|
|
129
|
+
return stage(project, { configuration, held, rooted });
|
|
117
130
|
});
|
|
118
131
|
if (failures.length > 0) {
|
|
119
132
|
throw new DiscoveryError(
|
|
@@ -121,15 +134,29 @@ export function runRoots(
|
|
|
121
134
|
failures,
|
|
122
135
|
);
|
|
123
136
|
}
|
|
137
|
+
return projects.filter((answer): answer is Answer => answer !== undefined);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The root set of the run the scope and the configuration describe.
|
|
142
|
+
*
|
|
143
|
+
* A configured string is a finding when it names nothing in every project, and the
|
|
144
|
+
* finding sits in the document that declared the roots.
|
|
145
|
+
*/
|
|
146
|
+
export function runRoots(
|
|
147
|
+
engine: Engine,
|
|
148
|
+
host: Host,
|
|
149
|
+
scope: Scope,
|
|
150
|
+
config: Config,
|
|
151
|
+
provenance: Provenance,
|
|
152
|
+
): RunRoots {
|
|
153
|
+
const targetRoot = scope.target.path;
|
|
154
|
+
const projects = readProjects(engine, host, scope, config, (_project, read) => read);
|
|
124
155
|
|
|
125
156
|
const configurations: string[] = [];
|
|
126
157
|
const merged = new Map<string, Omit<RunRoot, "configurations"> & { configurations: string[] }>();
|
|
127
158
|
for (const answer of projects) {
|
|
128
|
-
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const configuration =
|
|
132
|
-
relativePath(targetRoot, answer.rooted.configFile) ?? answer.rooted.configFile;
|
|
159
|
+
const configuration = answer.configuration;
|
|
133
160
|
configurations.push(configuration);
|
|
134
161
|
const symbols = new Map(answer.held.symbols.map((symbol) => [symbol.id, symbol]));
|
|
135
162
|
for (const root of answer.rooted.liveUnderReachability) {
|
|
@@ -159,7 +186,7 @@ export function runRoots(
|
|
|
159
186
|
);
|
|
160
187
|
const unmatched = unmatchedEverywhere(
|
|
161
188
|
config.rootPatterns,
|
|
162
|
-
projects.
|
|
189
|
+
projects.map((answer) => answer.rooted),
|
|
163
190
|
);
|
|
164
191
|
return {
|
|
165
192
|
configurations,
|
|
@@ -167,3 +194,43 @@ export function runRoots(
|
|
|
167
194
|
findings: unmatchedRoots(unmatched, documentOf(host, provenance, ROOTS_SETTING, targetRoot)),
|
|
168
195
|
};
|
|
169
196
|
}
|
|
197
|
+
|
|
198
|
+
/** One configuration's sweep, beside the declarations it judged. */
|
|
199
|
+
export interface ConfigurationSweep {
|
|
200
|
+
/** The name discovery gives the configuration's project. */
|
|
201
|
+
readonly configuration: string;
|
|
202
|
+
readonly symbols: readonly InventorySymbol[];
|
|
203
|
+
readonly sweep: SweepResult;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Sweeps every project of the run the scope and the configuration describe, one
|
|
208
|
+
* configuration at a time, in discovery order. Each project's references are read
|
|
209
|
+
* against its own inventory with the configured test-file patterns, and its graph is
|
|
210
|
+
* swept under the input the caller decided once for the run.
|
|
211
|
+
*/
|
|
212
|
+
export function runSweeps(
|
|
213
|
+
engine: Engine,
|
|
214
|
+
host: Host,
|
|
215
|
+
scope: Scope,
|
|
216
|
+
config: Config,
|
|
217
|
+
input: SweepInput,
|
|
218
|
+
): readonly ConfigurationSweep[] {
|
|
219
|
+
const targetRoot = scope.target.path;
|
|
220
|
+
return readProjects(engine, host, scope, config, (project, read) => {
|
|
221
|
+
const resolved = references(project, read.held, targetRoot, {
|
|
222
|
+
testFiles: config.ts.testFiles,
|
|
223
|
+
});
|
|
224
|
+
const graph = graphOf(
|
|
225
|
+
read.held.symbols,
|
|
226
|
+
resolved.references,
|
|
227
|
+
read.rooted.liveUnderReachability,
|
|
228
|
+
resolved.testFilePaths,
|
|
229
|
+
);
|
|
230
|
+
return {
|
|
231
|
+
configuration: read.configuration,
|
|
232
|
+
symbols: read.held.symbols,
|
|
233
|
+
sweep: sweep(graph, input),
|
|
234
|
+
};
|
|
235
|
+
});
|
|
236
|
+
}
|