@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
package/src/resolve.ts
CHANGED
|
@@ -38,6 +38,24 @@ const SEVERITY_KEY = /^DS[0-9]{2}([0-9]{2})?$/u;
|
|
|
38
38
|
const EXEMPTION_CLASS = /^[a-z][a-z0-9-]*$/u;
|
|
39
39
|
const CONTRACT_VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+$/u;
|
|
40
40
|
|
|
41
|
+
/** The members only a platform build configuration carries. */
|
|
42
|
+
const PLATFORM_MEMBERS = ["os", "arch", "tags"] as const;
|
|
43
|
+
|
|
44
|
+
/** The member only a project build configuration carries. */
|
|
45
|
+
const PROJECT_MEMBER = "project";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* One segment of a project path: not empty, not `.` or `..`, and holding no
|
|
49
|
+
* solidus, backslash or line break.
|
|
50
|
+
*/
|
|
51
|
+
const PROJECT_SEGMENT = String.raw`(?:[^/\\.\r\n][^/\\\r\n]*|\.[^/\\.\r\n][^/\\\r\n]*|\.\.[^/\\\r\n]+)`;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A project path: segments joined by a solidus, so the path is relative, below
|
|
55
|
+
* the root it is read against, and spelled one way on every machine.
|
|
56
|
+
*/
|
|
57
|
+
const PROJECT_PATH = new RegExp(`^${PROJECT_SEGMENT}(?:/${PROJECT_SEGMENT})*$`, "u");
|
|
58
|
+
|
|
41
59
|
const TARGET_KINDS: readonly TargetKind[] = ["application", "library"];
|
|
42
60
|
const CONFIDENCES: readonly Confidence[] = ["certain", "probable", "possible"];
|
|
43
61
|
const GENERATED_FILES: readonly GeneratedFiles[] = ["exclude", "include"];
|
|
@@ -252,10 +270,75 @@ function requireMember(value: string | undefined, path: string, label: string):
|
|
|
252
270
|
return value;
|
|
253
271
|
}
|
|
254
272
|
|
|
273
|
+
/** One build configuration entry as a platform: an identifier, an operating system and an architecture. */
|
|
274
|
+
function readPlatform(
|
|
275
|
+
entry: Record<string, unknown>,
|
|
276
|
+
at: string,
|
|
277
|
+
label: string,
|
|
278
|
+
): BuildConfiguration {
|
|
279
|
+
return {
|
|
280
|
+
shape: "platform",
|
|
281
|
+
id: requireMember(readString(entry, "id", `${at}.id`, label), `${at}.id`, label),
|
|
282
|
+
os: requireMember(readString(entry, "os", `${at}.os`, label), `${at}.os`, label),
|
|
283
|
+
arch: requireMember(readString(entry, "arch", `${at}.arch`, label), `${at}.arch`, label),
|
|
284
|
+
tags: readStrings(entry, "tags", `${at}.tags`, label, 0) ?? [],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** One build configuration entry as a project: an identifier and a configuration file below the target root. */
|
|
289
|
+
function readProject(
|
|
290
|
+
entry: Record<string, unknown>,
|
|
291
|
+
at: string,
|
|
292
|
+
label: string,
|
|
293
|
+
): BuildConfiguration {
|
|
294
|
+
const id = requireMember(readString(entry, "id", `${at}.id`, label), `${at}.id`, label);
|
|
295
|
+
const path = `${at}.${PROJECT_MEMBER}`;
|
|
296
|
+
const project = requireMember(readString(entry, PROJECT_MEMBER, path, label), path, label);
|
|
297
|
+
if (!PROJECT_PATH.test(project)) {
|
|
298
|
+
throw malformed(
|
|
299
|
+
label,
|
|
300
|
+
path,
|
|
301
|
+
`${JSON.stringify(project)} is not a path below the target root: segments joined by /, ` +
|
|
302
|
+
"none empty, . or .., and no backslash or line break",
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return { shape: "project", id, project };
|
|
306
|
+
}
|
|
307
|
+
|
|
255
308
|
/**
|
|
256
|
-
*
|
|
257
|
-
*
|
|
309
|
+
* One build configuration entry, read as the one shape its members name: a member
|
|
310
|
+
* only a platform carries makes it a platform, the member only a project carries
|
|
311
|
+
* makes it a project, and an entry naming members of both, or of neither, is
|
|
312
|
+
* refused at the entry, because the two shapes are exclusive.
|
|
258
313
|
*/
|
|
314
|
+
function readConfiguration(entry: unknown, at: string, label: string): BuildConfiguration {
|
|
315
|
+
if (!isRecord(entry)) {
|
|
316
|
+
throw malformed(label, at, "is not an object");
|
|
317
|
+
}
|
|
318
|
+
const platform = PLATFORM_MEMBERS.filter((name) => Object.hasOwn(entry, name));
|
|
319
|
+
const project = Object.hasOwn(entry, PROJECT_MEMBER);
|
|
320
|
+
if (platform.length > 0 && project) {
|
|
321
|
+
throw malformed(
|
|
322
|
+
label,
|
|
323
|
+
at,
|
|
324
|
+
`carries ${spell(platform)} of a platform and ${JSON.stringify(PROJECT_MEMBER)} of a ` +
|
|
325
|
+
"project, and an entry is one shape or the other",
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
if (project) {
|
|
329
|
+
return readProject(entry, at, label);
|
|
330
|
+
}
|
|
331
|
+
if (platform.length === 0) {
|
|
332
|
+
throw malformed(
|
|
333
|
+
label,
|
|
334
|
+
at,
|
|
335
|
+
`names neither a platform's "os" and "arch" nor a project's ${JSON.stringify(PROJECT_MEMBER)}`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return readPlatform(entry, at, label);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** The build matrix, one entry in one shape at a time. */
|
|
259
342
|
function readConfigurations(
|
|
260
343
|
analysis: Record<string, unknown> | undefined,
|
|
261
344
|
label: string,
|
|
@@ -267,18 +350,9 @@ function readConfigurations(
|
|
|
267
350
|
if (!Array.isArray(value)) {
|
|
268
351
|
throw malformed(label, "analysis.configurations", "is not an array");
|
|
269
352
|
}
|
|
270
|
-
return (value as unknown[]).map((entry, index) =>
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
throw malformed(label, at, "is not an object");
|
|
274
|
-
}
|
|
275
|
-
return {
|
|
276
|
-
id: requireMember(readString(entry, "id", `${at}.id`, label), `${at}.id`, label),
|
|
277
|
-
os: requireMember(readString(entry, "os", `${at}.os`, label), `${at}.os`, label),
|
|
278
|
-
arch: requireMember(readString(entry, "arch", `${at}.arch`, label), `${at}.arch`, label),
|
|
279
|
-
tags: readStrings(entry, "tags", `${at}.tags`, label, 0) ?? [],
|
|
280
|
-
};
|
|
281
|
-
});
|
|
353
|
+
return (value as unknown[]).map((entry, index) =>
|
|
354
|
+
readConfiguration(entry, `analysis.configurations[${String(index)}]`, label),
|
|
355
|
+
);
|
|
282
356
|
}
|
|
283
357
|
|
|
284
358
|
/**
|
|
@@ -642,6 +716,35 @@ function checkLabels(provenance: ReadonlyMap<string, Origin>): void {
|
|
|
642
716
|
* the usage exit code.
|
|
643
717
|
*/
|
|
644
718
|
export function resolve(inputs: Inputs): { config: Config; provenance: Provenance } {
|
|
719
|
+
const { config, provenance } = settleAll(inputs);
|
|
720
|
+
if (config.targetKind === "") {
|
|
721
|
+
throw new ConfigError(
|
|
722
|
+
"missing-target-kind",
|
|
723
|
+
"target.kind",
|
|
724
|
+
"target.kind is not set, it has no default and is never inferred: searched " +
|
|
725
|
+
`${describeSource("the repository configuration", inputs.repository, inputs.repositoryLabel)} and ` +
|
|
726
|
+
describeSource("the central configuration", inputs.central, inputs.centralLabel),
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
checkLabels(provenance);
|
|
730
|
+
return { config, provenance };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* The build matrix the configuration documents resolve to, every document read and
|
|
735
|
+
* refused as {@link resolve} refuses it, and nothing else required of them: which
|
|
736
|
+
* projects a run analyzes depends on the matrix alone, so a question about the
|
|
737
|
+
* projects asks no target kind.
|
|
738
|
+
*/
|
|
739
|
+
export function resolveMatrix(inputs: Inputs): readonly BuildConfiguration[] {
|
|
740
|
+
return settleAll(inputs).config.analysis.configurations;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Every setting the documents supply, each from the highest-ranked source carrying
|
|
745
|
+
* it, with the origin of each. The target kind is empty where no source names one.
|
|
746
|
+
*/
|
|
747
|
+
function settleAll(inputs: Inputs): { config: Config; provenance: Map<string, Origin> } {
|
|
645
748
|
const sources = readSources(inputs);
|
|
646
749
|
const defaults = defaultConfig();
|
|
647
750
|
const provenance = new Map<string, Origin>();
|
|
@@ -718,15 +821,5 @@ export function resolve(inputs: Inputs): { config: Config; provenance: Provenanc
|
|
|
718
821
|
},
|
|
719
822
|
};
|
|
720
823
|
|
|
721
|
-
if (config.targetKind === "") {
|
|
722
|
-
throw new ConfigError(
|
|
723
|
-
"missing-target-kind",
|
|
724
|
-
"target.kind",
|
|
725
|
-
"target.kind is not set, it has no default and is never inferred: searched " +
|
|
726
|
-
`${describeSource("the repository configuration", inputs.repository, inputs.repositoryLabel)} and ` +
|
|
727
|
-
describeSource("the central configuration", inputs.central, inputs.centralLabel),
|
|
728
|
-
);
|
|
729
|
-
}
|
|
730
|
-
checkLabels(provenance);
|
|
731
824
|
return { config, provenance };
|
|
732
825
|
}
|
package/src/run.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import type { Host } from "./host.ts";
|
|
10
10
|
import { joinPath, resolvePath } from "./paths.ts";
|
|
11
11
|
import { printConfig } from "./print.ts";
|
|
12
|
-
import { resolve } from "./resolve.ts";
|
|
12
|
+
import { resolve, resolveMatrix } from "./resolve.ts";
|
|
13
13
|
import { readScope, ScopeError, scopeForDir, type Scope } from "./scope.ts";
|
|
14
14
|
import { diagnosticsOf, openEngine, runSession, type Engine } from "./session.ts";
|
|
15
15
|
import { CONTRACT_VERSION } from "./version.ts";
|
|
@@ -245,8 +245,10 @@ function printConfigVerb(args: readonly string[], out: Writer, err: Writer, host
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
/**
|
|
248
|
-
* Writes the
|
|
249
|
-
*
|
|
248
|
+
* Writes the name of every project one run analyzes, one per line, having read each
|
|
249
|
+
* project's diagnostics first: the identifier the build matrix declares for it, or,
|
|
250
|
+
* where discovery derived the project, its configuration file's path below the
|
|
251
|
+
* target root.
|
|
250
252
|
*/
|
|
251
253
|
function printProjectsVerb(
|
|
252
254
|
args: readonly string[],
|
|
@@ -258,14 +260,16 @@ function printProjectsVerb(
|
|
|
258
260
|
let engine: Engine | undefined;
|
|
259
261
|
try {
|
|
260
262
|
const options = readOptions(args);
|
|
263
|
+
const matrix = resolveMatrix(inputsOf(host, options));
|
|
261
264
|
const scope = scopeOf(host, options);
|
|
262
265
|
engine = openClient(false);
|
|
263
|
-
const discovered = discoverProjects(engine, host, scope);
|
|
266
|
+
const discovered = discoverProjects(engine, host, scope, matrix);
|
|
267
|
+
const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
|
|
264
268
|
const failures: string[] = [];
|
|
265
269
|
const { projects } = runSession(engine, discovered.configFiles, (project) => {
|
|
266
270
|
const errors = diagnosticErrors(diagnosticsOf(project));
|
|
267
271
|
failures.push(...errors.map(renderDiagnostic));
|
|
268
|
-
return project.configFile;
|
|
272
|
+
return ids.get(project.configFile) ?? project.configFile;
|
|
269
273
|
});
|
|
270
274
|
if (failures.length > 0) {
|
|
271
275
|
for (const line of failures) {
|
|
@@ -274,8 +278,8 @@ function printProjectsVerb(
|
|
|
274
278
|
err.write(`deadset-ts: ${String(failures.length)} error(s); no answer was produced\n`);
|
|
275
279
|
return EXIT_FAILURE;
|
|
276
280
|
}
|
|
277
|
-
for (const
|
|
278
|
-
out.write(`${
|
|
281
|
+
for (const id of projects) {
|
|
282
|
+
out.write(`${id}\n`);
|
|
279
283
|
}
|
|
280
284
|
return EXIT_CLEAN;
|
|
281
285
|
} catch (error: unknown) {
|
package/src/schema.ts
CHANGED
|
@@ -8,7 +8,10 @@ import { ConfigError } from "./config.ts";
|
|
|
8
8
|
* holds no value of its own.
|
|
9
9
|
* - `map` is an object whose member names the key list leaves open, its values
|
|
10
10
|
* scalars: the severity and provenance objects.
|
|
11
|
-
* - `list` is an array whose entries are objects with declared members.
|
|
11
|
+
* - `list` is an array whose entries are objects with declared members. Where
|
|
12
|
+
* the key list admits an entry in more than one shape, the members are every
|
|
13
|
+
* shape's together, and which of them one entry may hold is the decoder's
|
|
14
|
+
* refusal, not the walk's.
|
|
12
15
|
* - `object` is one setting written as an object: its member names are declared
|
|
13
16
|
* and a source supplies the object whole.
|
|
14
17
|
*/
|
|
@@ -44,7 +47,7 @@ export const SCHEMA_ROOT: KeyNode = {
|
|
|
44
47
|
consumer_tests: leaf,
|
|
45
48
|
configurations: {
|
|
46
49
|
kind: "list",
|
|
47
|
-
members: { id: leaf, os: leaf, arch: leaf, tags: leaf },
|
|
50
|
+
members: { id: leaf, os: leaf, arch: leaf, tags: leaf, project: leaf },
|
|
48
51
|
},
|
|
49
52
|
matrix: { kind: "section", members: { complete: leaf } },
|
|
50
53
|
template_dirs: leaf,
|
package/src/sweep.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
|
|
9
|
+
import { componentsOf, type Component } from "./components.ts";
|
|
10
|
+
import { namesACaller, OUTSIDE, type Graph } from "./graph.ts";
|
|
11
|
+
|
|
12
|
+
/** The liveness relation that decided a declaration, as a finding spells it. */
|
|
13
|
+
export type Relation = "reference-counting" | "reachability";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The run's reference mode, decided once by the composition and read by every stage
|
|
17
|
+
* that counts references.
|
|
18
|
+
*/
|
|
19
|
+
export interface Mode {
|
|
20
|
+
/**
|
|
21
|
+
* Counts no reference a test file made, under either relation. Every reference a root
|
|
22
|
+
* in a test file makes is such a reference, so nothing only a test reaches is live.
|
|
23
|
+
*/
|
|
24
|
+
readonly production: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What one sweep runs over besides the graph. */
|
|
28
|
+
export interface SweepInput {
|
|
29
|
+
/**
|
|
30
|
+
* The declarations a matched suppression names. A mark holds its declaration live
|
|
31
|
+
* under both relations and seeds reachability through every reference it makes, so
|
|
32
|
+
* nothing only a marked declaration references is dead.
|
|
33
|
+
*/
|
|
34
|
+
readonly marked: readonly string[];
|
|
35
|
+
readonly mode: Mode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** One dead declaration, the relation that found it, and the counts a kind reads. */
|
|
39
|
+
export interface Candidate {
|
|
40
|
+
readonly id: string;
|
|
41
|
+
/** The references a production file made to the declaration, whatever the mode counts. */
|
|
42
|
+
readonly productionRefs: number;
|
|
43
|
+
/** The references a test file made to it. */
|
|
44
|
+
readonly testRefs: number;
|
|
45
|
+
/**
|
|
46
|
+
* Reference counting where the mode counts no reference to the declaration, and
|
|
47
|
+
* reachability otherwise, so a declaration dead under both carries the stronger claim.
|
|
48
|
+
*/
|
|
49
|
+
readonly relation: Relation;
|
|
50
|
+
/**
|
|
51
|
+
* Whether the test-of-dead-code rule admitted the declaration: a test declaration whose
|
|
52
|
+
* set of referenced production declarations is non-empty and wholly dead.
|
|
53
|
+
*/
|
|
54
|
+
readonly testOfDeadCode: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One sweep's answer. */
|
|
58
|
+
export interface SweepResult {
|
|
59
|
+
/**
|
|
60
|
+
* Per declaration, the relations that hold it live, reference counting first. A
|
|
61
|
+
* declaration neither holds live has no entry.
|
|
62
|
+
*/
|
|
63
|
+
readonly liveUnder: ReadonlyMap<string, readonly Relation[]>;
|
|
64
|
+
/** The dead declarations, in the order the inventory holds them. */
|
|
65
|
+
readonly candidates: readonly Candidate[];
|
|
66
|
+
/** The dead components, each before every component it reaches. */
|
|
67
|
+
readonly components: readonly Component[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Answers which declarations of one graph are dead under the input's mode.
|
|
72
|
+
*
|
|
73
|
+
* The order is the order the answers depend on: the callers and the marks are live
|
|
74
|
+
* before either relation runs, the candidate set is every judged declaration at least
|
|
75
|
+
* one relation does not hold live, the tests of dead code join it, and the components
|
|
76
|
+
* are computed over the set that results.
|
|
77
|
+
*/
|
|
78
|
+
export function sweep(graph: Graph, input: SweepInput): SweepResult {
|
|
79
|
+
const marked = graph.symbols.map(() => false);
|
|
80
|
+
for (const id of input.marked) {
|
|
81
|
+
const at = graph.at(id);
|
|
82
|
+
if (at !== OUTSIDE) {
|
|
83
|
+
marked[at] = true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Every root and every mark seeds reachability. A root of a kind that names a caller,
|
|
87
|
+
// and a mark, are live under reference counting as well; a root that only supposes a
|
|
88
|
+
// caller is not.
|
|
89
|
+
const called = graph.symbols.map(() => false);
|
|
90
|
+
for (const root of graph.rooted) {
|
|
91
|
+
if (namesACaller(root.kind)) {
|
|
92
|
+
called[root.at] = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const counted = (at: number): number => {
|
|
97
|
+
const made = graph.made[at];
|
|
98
|
+
if (made === undefined) {
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
return input.mode.production ? made.production : made.production + made.test;
|
|
102
|
+
};
|
|
103
|
+
const reached = reachable(graph, marked, input.mode);
|
|
104
|
+
const liveUnder = graph.symbols.map((_symbol, at): readonly Relation[] => {
|
|
105
|
+
const relations: Relation[] = [];
|
|
106
|
+
if (marked[at] === true || called[at] === true || counted(at) > 0) {
|
|
107
|
+
relations.push("reference-counting");
|
|
108
|
+
}
|
|
109
|
+
if (reached[at] === true) {
|
|
110
|
+
relations.push("reachability");
|
|
111
|
+
}
|
|
112
|
+
return relations;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const dead = graph.symbols.map(
|
|
116
|
+
(_symbol, at) => graph.subject[at] === true && (liveUnder[at]?.length ?? 0) < 2,
|
|
117
|
+
);
|
|
118
|
+
const testOfDeadCode = admitTestsOfDeadCode(graph, dead, marked);
|
|
119
|
+
|
|
120
|
+
const candidates: Candidate[] = [];
|
|
121
|
+
const live = new Map<string, readonly Relation[]>();
|
|
122
|
+
graph.symbols.forEach((symbol, at) => {
|
|
123
|
+
const relations = liveUnder[at] ?? [];
|
|
124
|
+
if (relations.length > 0) {
|
|
125
|
+
live.set(symbol.id, relations);
|
|
126
|
+
}
|
|
127
|
+
if (dead[at] !== true) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
candidates.push({
|
|
131
|
+
id: symbol.id,
|
|
132
|
+
productionRefs: graph.made[at]?.production ?? 0,
|
|
133
|
+
testRefs: graph.made[at]?.test ?? 0,
|
|
134
|
+
relation: counted(at) === 0 ? "reference-counting" : "reachability",
|
|
135
|
+
testOfDeadCode: testOfDeadCode[at] === true,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
liveUnder: live,
|
|
141
|
+
candidates,
|
|
142
|
+
components: componentsOf(graph, dead, testOfDeadCode),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Per declaration, whether a seed reaches it. The two seeds reach through different
|
|
148
|
+
* reference sets: a marked declaration is live by a mechanism the analysis cannot see, so
|
|
149
|
+
* no mode withholds a reference it makes, while a root reaches through the references the
|
|
150
|
+
* mode counts. The marks run first, so what they reach is expanded under the wider rule.
|
|
151
|
+
*/
|
|
152
|
+
function reachable(graph: Graph, marked: readonly boolean[], mode: Mode): readonly boolean[] {
|
|
153
|
+
const reached = graph.symbols.map(() => false);
|
|
154
|
+
const expanded = graph.symbols.map(() => false);
|
|
155
|
+
const queue: number[] = [];
|
|
156
|
+
|
|
157
|
+
// A reference to a file is a reference to the module's namespace, which reaches what
|
|
158
|
+
// the module exports as well as its top level. A root does not expand: a file a
|
|
159
|
+
// runtime runs without reading its exports is a root by itself.
|
|
160
|
+
const enter = (at: number, viaReference: boolean): void => {
|
|
161
|
+
if (!reached[at]) {
|
|
162
|
+
reached[at] = true;
|
|
163
|
+
queue.push(at);
|
|
164
|
+
}
|
|
165
|
+
if (!viaReference || expanded[at] === true) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
expanded[at] = true;
|
|
169
|
+
for (const exported of graph.exportsOf[at] ?? []) {
|
|
170
|
+
enter(exported, false);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const walk = (held: boolean): void => {
|
|
174
|
+
for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
|
|
175
|
+
for (const edge of graph.out[at] ?? []) {
|
|
176
|
+
if (!held && mode.production && edge.test) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
enter(edge.to, true);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
marked.forEach((mark, at) => {
|
|
185
|
+
if (mark) {
|
|
186
|
+
enter(at, false);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
walk(true);
|
|
190
|
+
for (const root of graph.rooted) {
|
|
191
|
+
enter(root.at, false);
|
|
192
|
+
}
|
|
193
|
+
walk(false);
|
|
194
|
+
return reached;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Admits every test declaration whose set of referenced production declarations is
|
|
199
|
+
* non-empty and wholly dead, and returns which it admitted. The set is read from every
|
|
200
|
+
* 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 admitted.
|
|
202
|
+
*/
|
|
203
|
+
function admitTestsOfDeadCode(
|
|
204
|
+
graph: Graph,
|
|
205
|
+
dead: boolean[],
|
|
206
|
+
marked: readonly boolean[],
|
|
207
|
+
): readonly boolean[] {
|
|
208
|
+
const admitted = graph.symbols.map(() => false);
|
|
209
|
+
graph.symbols.forEach((_symbol, at) => {
|
|
210
|
+
if (graph.test[at] !== true || graph.subject[at] !== true || marked[at] === true) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let targets = 0;
|
|
214
|
+
let live = 0;
|
|
215
|
+
for (const edge of graph.out[at] ?? []) {
|
|
216
|
+
if (graph.test[edge.to] === true || graph.subject[edge.to] !== true) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
targets += 1;
|
|
220
|
+
if (dead[edge.to] !== true) {
|
|
221
|
+
live += 1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (targets > 0 && live === 0) {
|
|
225
|
+
dead[at] = true;
|
|
226
|
+
admitted[at] = true;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return admitted;
|
|
230
|
+
}
|
package/src/version.ts
CHANGED