@cplieger/deadset-ts 2.2.1 → 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 +9 -16
- package/dist/src/analysis.js +142 -0
- package/dist/src/components.js +289 -0
- package/dist/src/config.js +6 -0
- package/dist/src/discover.js +98 -24
- package/dist/src/entry-point-gaps.js +278 -0
- package/dist/src/entry-points.js +385 -0
- package/dist/src/finding.js +12 -0
- package/dist/src/glob.js +65 -0
- package/dist/src/graph.js +116 -0
- package/dist/src/index.js +1 -2
- package/dist/src/manifest.js +120 -0
- package/dist/src/print.js +3 -6
- package/dist/src/references.js +156 -106
- package/dist/src/resolve.js +82 -20
- package/dist/src/roots.js +345 -0
- package/dist/src/run.js +59 -10
- package/dist/src/schema.js +1 -1
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +122 -0
- package/dist/src/sweep.js +156 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +3 -3
- package/src/analysis.ts +236 -0
- package/src/components.ts +372 -0
- package/src/config.ts +30 -2
- package/src/discover.ts +144 -29
- package/src/entry-point-gaps.ts +304 -0
- package/src/entry-points.ts +496 -0
- package/src/finding.ts +43 -0
- package/src/glob.ts +67 -0
- package/src/graph.ts +178 -0
- package/src/index.ts +1 -20
- package/src/manifest.ts +174 -0
- package/src/print.ts +5 -6
- package/src/references.ts +201 -111
- package/src/resolve.ts +117 -24
- package/src/roots.ts +467 -0
- package/src/run.ts +66 -11
- package/src/schema.ts +5 -2
- package/src/session.ts +6 -1
- package/src/source-files.ts +157 -0
- package/src/sweep.ts +230 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The target's own source files of one project, looked up the ways a path written
|
|
3
|
+
* in a document or a string names one: as the file itself, or as a file the
|
|
4
|
+
* compiler emits from it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { SourceFile } from "@typescript/native/unstable/ast";
|
|
8
|
+
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
9
|
+
import type { ProjectView } from "./session.ts";
|
|
10
|
+
|
|
11
|
+
/** The source extensions the compiler emits each output extension from. */
|
|
12
|
+
const EMITTED: readonly { readonly emitted: string; readonly sources: readonly string[] }[] = [
|
|
13
|
+
{ emitted: ".d.ts", sources: [".ts", ".tsx"] },
|
|
14
|
+
{ emitted: ".d.mts", sources: [".mts"] },
|
|
15
|
+
{ emitted: ".d.cts", sources: [".cts"] },
|
|
16
|
+
{ emitted: ".js", sources: [".ts", ".tsx"] },
|
|
17
|
+
{ emitted: ".jsx", sources: [".tsx"] },
|
|
18
|
+
{ emitted: ".mjs", sources: [".mts"] },
|
|
19
|
+
{ emitted: ".cjs", sources: [".cts"] },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/** The one character a subpath pattern of a manifest's `exports` holds. */
|
|
23
|
+
const SUBPATH_WILDCARD = "*";
|
|
24
|
+
|
|
25
|
+
/** One path with its extension replaced, where it carries the one named. */
|
|
26
|
+
function reExtended(path: string, emitted: string, source: string): string | undefined {
|
|
27
|
+
return path.endsWith(emitted) ? path.slice(0, path.length - emitted.length) + source : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The longest directory every path shares, which is the directory the compiler
|
|
32
|
+
* emits from where the configuration declares no root directory.
|
|
33
|
+
*/
|
|
34
|
+
function commonDirectory(paths: readonly string[]): string {
|
|
35
|
+
const first = paths[0];
|
|
36
|
+
if (first === undefined) {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
let common = dirnamePath(first).split("/");
|
|
40
|
+
for (const path of paths.slice(1)) {
|
|
41
|
+
const segments = dirnamePath(path).split("/");
|
|
42
|
+
let shared = 0;
|
|
43
|
+
while (
|
|
44
|
+
shared < common.length &&
|
|
45
|
+
shared < segments.length &&
|
|
46
|
+
common[shared] === segments[shared]
|
|
47
|
+
) {
|
|
48
|
+
shared += 1;
|
|
49
|
+
}
|
|
50
|
+
common = common.slice(0, shared);
|
|
51
|
+
}
|
|
52
|
+
return common.join("/");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether one path is what a subpath pattern stands for: the text before the
|
|
57
|
+
* wildcard, any run of characters including the solidus, then the text after it.
|
|
58
|
+
*/
|
|
59
|
+
function matchesSubpath(pattern: string, path: string): boolean {
|
|
60
|
+
const at = pattern.indexOf(SUBPATH_WILDCARD);
|
|
61
|
+
const before = pattern.slice(0, at);
|
|
62
|
+
const after = pattern.slice(at + SUBPATH_WILDCARD.length);
|
|
63
|
+
return (
|
|
64
|
+
path.length >= before.length + after.length && path.startsWith(before) && path.endsWith(after)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One project's own source files, and the lookups a written path is resolved through. */
|
|
69
|
+
export interface SourceFiles {
|
|
70
|
+
/** Every own file, by its path below the target root, in the order the program holds them. */
|
|
71
|
+
readonly byPath: ReadonlyMap<string, SourceFile>;
|
|
72
|
+
/** Every own file, by the absolute path the program names it by. */
|
|
73
|
+
readonly byName: ReadonlyMap<string, SourceFile>;
|
|
74
|
+
/**
|
|
75
|
+
* The own files one absolute path names: the file at that path, or the source the
|
|
76
|
+
* project would emit a file at that path from. A path holding the wildcard of a
|
|
77
|
+
* subpath pattern names every file it stands for. A path no rule reaches names
|
|
78
|
+
* nothing.
|
|
79
|
+
*/
|
|
80
|
+
named(path: string): readonly SourceFile[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The lookups over one project's own files.
|
|
85
|
+
*
|
|
86
|
+
* A path that names an emitted file is read back to the source the project would
|
|
87
|
+
* emit it from through the project's own emit mapping and nothing else: the
|
|
88
|
+
* extension is one the compiler emits from a source extension, and a path below the
|
|
89
|
+
* output directory is read below the directory the compiler emits from, which the
|
|
90
|
+
* configuration declares or which is the directory every input file shares.
|
|
91
|
+
*/
|
|
92
|
+
export function sourceFilesOf<Brand>(project: ProjectView<Brand>, targetRoot: string): SourceFiles {
|
|
93
|
+
const byName = new Map<string, SourceFile>();
|
|
94
|
+
const byPath = new Map<string, SourceFile>();
|
|
95
|
+
for (const file of project.ownSourceFiles()) {
|
|
96
|
+
byName.set(file.fileName, file);
|
|
97
|
+
const path = relativePath(targetRoot, file.fileName);
|
|
98
|
+
if (path !== undefined) {
|
|
99
|
+
byPath.set(path, file);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const options = project.program.getCompilerOptions();
|
|
104
|
+
const configDir = dirnamePath(project.configFile);
|
|
105
|
+
const at = (option: string | undefined): string | undefined =>
|
|
106
|
+
option === undefined ? undefined : resolvePath(configDir, option);
|
|
107
|
+
const outDir = at(options.outDir);
|
|
108
|
+
const declarationDir = at(options.declarationDir);
|
|
109
|
+
const sourceDir = at(options.rootDir) ?? commonDirectory([...byName.keys()]);
|
|
110
|
+
|
|
111
|
+
/** One path read below the directory the compiler emits from, where it is below the one it emits to. */
|
|
112
|
+
const fromEmit = (path: string, emitted: string): string | undefined => {
|
|
113
|
+
const emitDir = emitted.startsWith(".d.") ? (declarationDir ?? outDir) : outDir;
|
|
114
|
+
if (emitDir === undefined) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const below = relativePath(emitDir, path);
|
|
118
|
+
return below === undefined ? undefined : joinPath(sourceDir, below);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** The source paths one written path may stand for, the path itself first. */
|
|
122
|
+
const candidates = (path: string): readonly string[] => {
|
|
123
|
+
const found = [path];
|
|
124
|
+
for (const { emitted, sources } of EMITTED) {
|
|
125
|
+
const mapped = fromEmit(path, emitted);
|
|
126
|
+
for (const source of sources) {
|
|
127
|
+
for (const from of [path, mapped]) {
|
|
128
|
+
const candidate = from === undefined ? undefined : reExtended(from, emitted, source);
|
|
129
|
+
if (candidate !== undefined) {
|
|
130
|
+
found.push(candidate);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return found;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
byPath,
|
|
140
|
+
byName,
|
|
141
|
+
named: (path) => {
|
|
142
|
+
if (!path.includes(SUBPATH_WILDCARD)) {
|
|
143
|
+
for (const candidate of candidates(path)) {
|
|
144
|
+
const held = byName.get(candidate);
|
|
145
|
+
if (held !== undefined) {
|
|
146
|
+
return [held];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
const patterns = candidates(path).filter((candidate) => candidate.includes(SUBPATH_WILDCARD));
|
|
152
|
+
return [...byName.values()].filter((file) =>
|
|
153
|
+
patterns.some((pattern) => matchesSubpath(pattern, file.fileName)),
|
|
154
|
+
);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
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