@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 +3 -3
- package/dist/src/alias-chain.js +96 -0
- package/dist/src/analysis.js +32 -17
- package/dist/src/exempt-classes.js +36 -0
- package/dist/src/exempt.js +132 -0
- package/dist/src/graph.js +5 -1
- package/dist/src/matrix.js +138 -0
- package/dist/src/references.js +78 -100
- package/dist/src/roots.js +14 -37
- package/dist/src/run.js +33 -1
- package/dist/src/session.js +16 -5
- package/dist/src/sweep.js +47 -41
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +6 -6
- package/src/alias-chain.ts +135 -0
- package/src/analysis.ts +53 -32
- package/src/components.ts +1 -1
- package/src/exempt-classes.ts +69 -0
- package/src/exempt.ts +200 -0
- package/src/graph.ts +12 -3
- package/src/matrix.ts +212 -0
- package/src/references.ts +108 -113
- package/src/roots.ts +15 -46
- package/src/run.ts +39 -1
- package/src/session.ts +18 -5
- package/src/sweep.ts +75 -46
- package/src/version.ts +1 -1
package/src/run.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runRoots, type RunRoot } from "./analysis.ts";
|
|
1
|
+
import { runRoots, runSweep, type RunRoot } from "./analysis.ts";
|
|
2
2
|
import { ConfigError, REPOSITORY_DOCUMENT, type Inputs } from "./config.ts";
|
|
3
3
|
import {
|
|
4
4
|
diagnosticErrors,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
DiscoveryError,
|
|
7
7
|
renderDiagnostic,
|
|
8
8
|
} from "./discover.ts";
|
|
9
|
+
import { retainedLines } from "./exempt.ts";
|
|
9
10
|
import type { Host } from "./host.ts";
|
|
10
11
|
import { joinPath, resolvePath } from "./paths.ts";
|
|
11
12
|
import { printConfig } from "./print.ts";
|
|
@@ -221,6 +222,9 @@ export function run(
|
|
|
221
222
|
if (verb === "print-roots") {
|
|
222
223
|
return printRootsVerb(args.slice(1), out, err, host, openClient);
|
|
223
224
|
}
|
|
225
|
+
if (verb === "print-retained") {
|
|
226
|
+
return printRetainedVerb(args.slice(1), out, err, host, openClient);
|
|
227
|
+
}
|
|
224
228
|
if (verb !== undefined && (VERBS as readonly string[]).includes(verb)) {
|
|
225
229
|
err.write(`deadset-ts: ${verb} is not implemented\n`);
|
|
226
230
|
return EXIT_USAGE;
|
|
@@ -337,6 +341,40 @@ function printRootsVerb(
|
|
|
337
341
|
}
|
|
338
342
|
}
|
|
339
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Writes every declaration an exemption held back in the production sweep a report is
|
|
346
|
+
* built from, one per line, and nothing where no exemption held one back. One fact
|
|
347
|
+
* several configurations found is one record, so the line names no configuration. The
|
|
348
|
+
* line shape is this command's own rather than a Contract format, so that sort and cut
|
|
349
|
+
* read it.
|
|
350
|
+
*/
|
|
351
|
+
function printRetainedVerb(
|
|
352
|
+
args: readonly string[],
|
|
353
|
+
out: Writer,
|
|
354
|
+
err: Writer,
|
|
355
|
+
host: Host,
|
|
356
|
+
openClient: (collectTiming: boolean) => Engine,
|
|
357
|
+
): number {
|
|
358
|
+
let engine: Engine | undefined;
|
|
359
|
+
try {
|
|
360
|
+
const options = readOptions(args);
|
|
361
|
+
const { config } = resolve(inputsOf(host, options));
|
|
362
|
+
const scope = scopeOf(host, options);
|
|
363
|
+
engine = openClient(false);
|
|
364
|
+
const swept = runSweep(engine, host, scope, config, {
|
|
365
|
+
marked: [],
|
|
366
|
+
mode: { production: true },
|
|
367
|
+
});
|
|
368
|
+
for (const line of retainedLines(swept.matrix.union.symbols, swept.retained)) {
|
|
369
|
+
out.write(`${line}\n`);
|
|
370
|
+
}
|
|
371
|
+
return EXIT_CLEAN;
|
|
372
|
+
} catch (error: unknown) {
|
|
373
|
+
engine?.close();
|
|
374
|
+
return refuse(error, err);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
340
378
|
/** Turns one refusal into the exit code the Contract's table gives it. */
|
|
341
379
|
function refuse(error: unknown, err: Writer): number {
|
|
342
380
|
if (error instanceof ConfigError) {
|
package/src/session.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from "@typescript/native/unstable/sync";
|
|
13
13
|
import { isIdentifier } from "@typescript/native/unstable/ast";
|
|
14
14
|
import type { Node, SourceFile } from "@typescript/native/unstable/ast";
|
|
15
|
+
import { DiscoveryError } from "./discover.ts";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* The compiler the analysis reads through. It is a client over a protocol to a
|
|
@@ -218,13 +219,16 @@ export interface SessionResult<Result> {
|
|
|
218
219
|
|
|
219
220
|
/**
|
|
220
221
|
* Runs `visit` over every project the compiler configurations name, inside one
|
|
221
|
-
* snapshot of one client.
|
|
222
|
+
* snapshot of one client, in the order they are named, and answers in that order.
|
|
222
223
|
*
|
|
223
224
|
* The snapshot is opened once with every configuration, so each becomes a project
|
|
224
225
|
* with its own program and checker; it is never updated, because the analysis
|
|
225
|
-
* reads and the tree does not change under it.
|
|
226
|
-
*
|
|
227
|
-
*
|
|
226
|
+
* reads and the tree does not change under it. The order the snapshot lists its
|
|
227
|
+
* projects in is the compiler's own, so each named configuration is looked up in it
|
|
228
|
+
* instead, and a configuration the snapshot opened no project for ends the run: the
|
|
229
|
+
* run would otherwise answer for fewer projects than it was asked to analyze. The
|
|
230
|
+
* snapshot is disposed once when the last project has been visited, and the client
|
|
231
|
+
* is released after it, whether the visit completed or threw.
|
|
228
232
|
*/
|
|
229
233
|
export function runSession<Result>(
|
|
230
234
|
engine: Engine,
|
|
@@ -234,7 +238,16 @@ export function runSession<Result>(
|
|
|
234
238
|
try {
|
|
235
239
|
const snapshot = engine.updateSnapshot(configFiles);
|
|
236
240
|
try {
|
|
237
|
-
const
|
|
241
|
+
const opened = new Map(
|
|
242
|
+
snapshot.getProjects().map((project) => [project.configFileName, project]),
|
|
243
|
+
);
|
|
244
|
+
const projects = configFiles.map((configFile) => {
|
|
245
|
+
const project = opened.get(configFile);
|
|
246
|
+
if (project === undefined) {
|
|
247
|
+
throw new DiscoveryError(`${configFile} opened no project`, []);
|
|
248
|
+
}
|
|
249
|
+
return visit(viewOf(project));
|
|
250
|
+
});
|
|
238
251
|
return { projects, timing: engine.getTimingInfo() };
|
|
239
252
|
} finally {
|
|
240
253
|
snapshot.dispose();
|
package/src/sweep.ts
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Which declarations of one graph are dead, under which liveness relation
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Which declarations of one graph are dead, and under which liveness relation.
|
|
3
|
+
* Reference counting holds a declaration live when any declaration references it,
|
|
4
|
+
* whatever that one's own liveness, or when it is a root of a kind that names a caller;
|
|
5
5
|
* reachability holds it live when a path of references reaches it from a root. So a
|
|
6
6
|
* declaration only unreachable ones reference is live under the first, dead under the second.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import type { TSExemptionClass } from "./exempt-classes.ts";
|
|
10
10
|
import { namesACaller, OUTSIDE, type Graph } from "./graph.ts";
|
|
11
|
+
import type { Position } from "./position.ts";
|
|
11
12
|
|
|
12
13
|
/** The liveness relation that decided a declaration, as a finding spells it. */
|
|
13
14
|
export type Relation = "reference-counting" | "reachability";
|
|
14
15
|
|
|
16
|
+
/** One declaration an exemption class holds back, and the evidence it was found by. */
|
|
17
|
+
export interface Exemption {
|
|
18
|
+
/** The identifier of the declaration held back. */
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly class: TSExemptionClass;
|
|
21
|
+
/** One clause naming the relation, then the thing it relates to: `passed to JSON.stringify`. */
|
|
22
|
+
readonly detail: string;
|
|
23
|
+
/** Where the evidence is written. */
|
|
24
|
+
readonly site: Position;
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
/**
|
|
16
28
|
* The run's reference mode, decided once by the composition and read by every stage
|
|
17
29
|
* that counts references.
|
|
@@ -32,6 +44,13 @@ export interface SweepInput {
|
|
|
32
44
|
* nothing only a marked declaration references is dead.
|
|
33
45
|
*/
|
|
34
46
|
readonly marked: readonly string[];
|
|
47
|
+
/**
|
|
48
|
+
* The exemptions the run computed. An exempt declaration is never a candidate, and it
|
|
49
|
+
* seeds reachability as a mark does, so nothing it references is dead; it is live
|
|
50
|
+
* under reference counting only where something references it. Absent, the sweep
|
|
51
|
+
* holds nothing back.
|
|
52
|
+
*/
|
|
53
|
+
readonly exempt?: readonly Exemption[];
|
|
35
54
|
readonly mode: Mode;
|
|
36
55
|
}
|
|
37
56
|
|
|
@@ -54,8 +73,8 @@ export interface Candidate {
|
|
|
54
73
|
readonly testOfDeadCode: boolean;
|
|
55
74
|
}
|
|
56
75
|
|
|
57
|
-
/** One
|
|
58
|
-
export interface
|
|
76
|
+
/** One graph's answer: what each relation holds live, and what is dead. */
|
|
77
|
+
export interface Liveness {
|
|
59
78
|
/**
|
|
60
79
|
* Per declaration, the relations that hold it live, reference counting first. A
|
|
61
80
|
* declaration neither holds live has no entry.
|
|
@@ -63,8 +82,6 @@ export interface SweepResult {
|
|
|
63
82
|
readonly liveUnder: ReadonlyMap<string, readonly Relation[]>;
|
|
64
83
|
/** The dead declarations, in the order the inventory holds them. */
|
|
65
84
|
readonly candidates: readonly Candidate[];
|
|
66
|
-
/** The dead components, each before every component it reaches. */
|
|
67
|
-
readonly components: readonly Component[];
|
|
68
85
|
}
|
|
69
86
|
|
|
70
87
|
/**
|
|
@@ -72,20 +89,19 @@ export interface SweepResult {
|
|
|
72
89
|
*
|
|
73
90
|
* The order is the order the answers depend on: the callers and the marks are live
|
|
74
91
|
* before either relation runs, the candidate set is every judged declaration at least
|
|
75
|
-
* one relation does not hold live
|
|
76
|
-
*
|
|
92
|
+
* one relation does not hold live and no exemption holds back, and the tests of dead
|
|
93
|
+
* code join it.
|
|
77
94
|
*/
|
|
78
|
-
export function sweep(graph: Graph, input: SweepInput):
|
|
79
|
-
const marked = graph.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// caller is not.
|
|
95
|
+
export function sweep(graph: Graph, input: SweepInput): Liveness {
|
|
96
|
+
const marked = flagged(graph, input.marked);
|
|
97
|
+
const exempt = flagged(
|
|
98
|
+
graph,
|
|
99
|
+
(input.exempt ?? []).map((record) => record.id),
|
|
100
|
+
);
|
|
101
|
+
const held = marked.map((mark, at) => mark || exempt[at] === true);
|
|
102
|
+
// Every root, every mark and every exemption seeds reachability. A root of a kind that
|
|
103
|
+
// names a caller, and a mark, are live under reference counting as well; a root that
|
|
104
|
+
// only supposes a caller is not, and neither is an exemption.
|
|
89
105
|
const called = graph.symbols.map(() => false);
|
|
90
106
|
for (const root of graph.rooted) {
|
|
91
107
|
if (namesACaller(root.kind)) {
|
|
@@ -100,7 +116,7 @@ export function sweep(graph: Graph, input: SweepInput): SweepResult {
|
|
|
100
116
|
}
|
|
101
117
|
return input.mode.production ? made.production : made.production + made.test;
|
|
102
118
|
};
|
|
103
|
-
const reached = reachable(graph,
|
|
119
|
+
const reached = reachable(graph, held, input.mode);
|
|
104
120
|
const liveUnder = graph.symbols.map((_symbol, at): readonly Relation[] => {
|
|
105
121
|
const relations: Relation[] = [];
|
|
106
122
|
if (marked[at] === true || called[at] === true || counted(at) > 0) {
|
|
@@ -113,9 +129,10 @@ export function sweep(graph: Graph, input: SweepInput): SweepResult {
|
|
|
113
129
|
});
|
|
114
130
|
|
|
115
131
|
const dead = graph.symbols.map(
|
|
116
|
-
(_symbol, at) =>
|
|
132
|
+
(_symbol, at) =>
|
|
133
|
+
graph.subject[at] === true && exempt[at] !== true && (liveUnder[at]?.length ?? 0) < 2,
|
|
117
134
|
);
|
|
118
|
-
const testOfDeadCode = admitTestsOfDeadCode(graph, dead,
|
|
135
|
+
const testOfDeadCode = admitTestsOfDeadCode(graph, dead, held);
|
|
119
136
|
|
|
120
137
|
const candidates: Candidate[] = [];
|
|
121
138
|
const live = new Map<string, readonly Relation[]>();
|
|
@@ -136,33 +153,44 @@ export function sweep(graph: Graph, input: SweepInput): SweepResult {
|
|
|
136
153
|
});
|
|
137
154
|
});
|
|
138
155
|
|
|
139
|
-
return {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
156
|
+
return { liveUnder: live, candidates };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Per declaration of the graph, whether one of the identifiers names it. */
|
|
160
|
+
function flagged(graph: Graph, ids: readonly string[]): boolean[] {
|
|
161
|
+
const flags = graph.symbols.map(() => false);
|
|
162
|
+
for (const id of ids) {
|
|
163
|
+
const at = graph.at(id);
|
|
164
|
+
if (at !== OUTSIDE) {
|
|
165
|
+
flags[at] = true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return flags;
|
|
144
169
|
}
|
|
145
170
|
|
|
146
171
|
/**
|
|
147
172
|
* Per declaration, whether a seed reaches it. The two seeds reach through different
|
|
148
|
-
* reference sets: a
|
|
149
|
-
* no mode withholds a reference it makes,
|
|
150
|
-
*
|
|
173
|
+
* reference sets: a held declaration, marked or exempt, is live by a mechanism the
|
|
174
|
+
* analysis cannot see, so no mode withholds a reference it makes, a test declaration's
|
|
175
|
+
* included, while a root reaches through the references the mode counts. The held seed
|
|
176
|
+
* runs first, so what it reaches is expanded under the wider rule.
|
|
151
177
|
*/
|
|
152
|
-
function reachable(graph: Graph,
|
|
178
|
+
function reachable(graph: Graph, held: readonly boolean[], mode: Mode): readonly boolean[] {
|
|
153
179
|
const reached = graph.symbols.map(() => false);
|
|
154
180
|
const expanded = graph.symbols.map(() => false);
|
|
155
181
|
const queue: number[] = [];
|
|
156
182
|
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
183
|
+
// Reaching a file reaches its top level: the references written outside every
|
|
184
|
+
// declaration it holds. A reference that reads a file reads the module's namespace,
|
|
185
|
+
// which reaches what the module exports as well. An evaluation runs the top level
|
|
186
|
+
// and reads no export, and a root does not expand either: a file a runtime runs
|
|
187
|
+
// without reading its exports is a root by itself.
|
|
188
|
+
const enter = (at: number, expands: boolean): void => {
|
|
161
189
|
if (!reached[at]) {
|
|
162
190
|
reached[at] = true;
|
|
163
191
|
queue.push(at);
|
|
164
192
|
}
|
|
165
|
-
if (!
|
|
193
|
+
if (!expands || expanded[at] === true) {
|
|
166
194
|
return;
|
|
167
195
|
}
|
|
168
196
|
expanded[at] = true;
|
|
@@ -170,19 +198,19 @@ function reachable(graph: Graph, marked: readonly boolean[], mode: Mode): readon
|
|
|
170
198
|
enter(exported, false);
|
|
171
199
|
}
|
|
172
200
|
};
|
|
173
|
-
const walk = (
|
|
201
|
+
const walk = (holding: boolean): void => {
|
|
174
202
|
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
175
203
|
for (const edge of graph.out[at] ?? []) {
|
|
176
|
-
if (!
|
|
204
|
+
if (!holding && mode.production && edge.test) {
|
|
177
205
|
continue;
|
|
178
206
|
}
|
|
179
|
-
enter(edge.to,
|
|
207
|
+
enter(edge.to, !edge.evaluation);
|
|
180
208
|
}
|
|
181
209
|
}
|
|
182
210
|
};
|
|
183
211
|
|
|
184
|
-
|
|
185
|
-
if (
|
|
212
|
+
held.forEach((hold, at) => {
|
|
213
|
+
if (hold) {
|
|
186
214
|
enter(at, false);
|
|
187
215
|
}
|
|
188
216
|
});
|
|
@@ -198,16 +226,17 @@ function reachable(graph: Graph, marked: readonly boolean[], mode: Mode): readon
|
|
|
198
226
|
* Admits every test declaration whose set of referenced production declarations is
|
|
199
227
|
* non-empty and wholly dead, and returns which it admitted. The set is read from every
|
|
200
228
|
* reference the test makes, not the ones the mode counts: a production sweep counts none,
|
|
201
|
-
* which is what makes the targets dead. A test referencing one live target is never
|
|
229
|
+
* which is what makes the targets dead. A test referencing one live target is never
|
|
230
|
+
* admitted, and neither is a marked or exempt test.
|
|
202
231
|
*/
|
|
203
232
|
function admitTestsOfDeadCode(
|
|
204
233
|
graph: Graph,
|
|
205
234
|
dead: boolean[],
|
|
206
|
-
|
|
235
|
+
held: readonly boolean[],
|
|
207
236
|
): readonly boolean[] {
|
|
208
237
|
const admitted = graph.symbols.map(() => false);
|
|
209
238
|
graph.symbols.forEach((_symbol, at) => {
|
|
210
|
-
if (graph.test[at] !== true || graph.subject[at] !== true ||
|
|
239
|
+
if (graph.test[at] !== true || graph.subject[at] !== true || held[at] === true) {
|
|
211
240
|
return;
|
|
212
241
|
}
|
|
213
242
|
let targets = 0;
|
package/src/version.ts
CHANGED