@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/roots.ts CHANGED
@@ -2,15 +2,15 @@
2
2
  * The root set of one project: every declaration the analysis keeps live without a
3
3
  * reference, and why.
4
4
  *
5
- * A root makes its declaration live under REACHABILITY and plays no part in
6
- * reference counting, which asks only whether some declaration of the loaded graph
7
- * names it. The two relations are different sets, so an exported declaration that
8
- * something outside the program reaches is live under the first and still a
9
- * candidate under the second.
5
+ * Every root seeds REACHABILITY, and no root counts as a reference. A root of a kind
6
+ * that names a caller is live under reference counting as well, which the sweep
7
+ * decides; a root that only supposes a consumer is not, so an exported declaration
8
+ * something outside the program reaches is live under reachability and still a
9
+ * candidate under reference counting.
10
10
  */
11
11
 
12
12
  import type { SourceFile } from "@typescript/native/unstable/ast";
13
- import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
13
+ import { aliasChains } from "./alias-chain.ts";
14
14
  import { REPOSITORY_DOCUMENT } from "./config.ts";
15
15
  import { entryPoints, type EntryRule } from "./entry-points.ts";
16
16
  import type { Finding } from "./finding.ts";
@@ -63,7 +63,8 @@ export interface Roots {
63
63
  * reason it is live.
64
64
  *
65
65
  * The member says which relation the set feeds: every declaration here is live
66
- * under reachability, and nothing here is live under reference counting.
66
+ * under reachability. A root of a kind that names a caller is also live under
67
+ * reference counting, which the sweep decides.
67
68
  */
68
69
  readonly liveUnderReachability: readonly Root[];
69
70
  /**
@@ -230,12 +231,10 @@ export function roots<Brand>(
230
231
  enter(point.file, point.rule, point.source, point.exports);
231
232
  }
232
233
 
233
- const exportsOf = exportTables(
234
- project,
235
- held,
236
- [...entered.filter((entry) => entry.exports).map((entry) => entry.file), ...published],
237
- files.byName,
238
- );
234
+ const exportsOf = exportTables(project, held, [
235
+ ...entered.filter((entry) => entry.exports).map((entry) => entry.file),
236
+ ...published,
237
+ ]);
239
238
  for (const entry of entered) {
240
239
  const id = fileIds.get(entry.file.fileName);
241
240
  if (id === undefined) {
@@ -282,7 +281,6 @@ function exportTables<Brand>(
282
281
  project: ProjectView<Brand>,
283
282
  held: Inventory,
284
283
  wanted: readonly SourceFile[],
285
- ownFiles: ReadonlyMap<string, SourceFile>,
286
284
  ): ReadonlyMap<string, readonly string[]> {
287
285
  const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
288
286
  const tables = new Map<string, readonly string[]>();
@@ -290,36 +288,7 @@ function exportTables<Brand>(
290
288
  return tables;
291
289
  }
292
290
  const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
293
- const chains = new Map<number, readonly string[]>();
294
-
295
- const chainOf = (symbol: TSSymbol): readonly string[] => {
296
- const cached = chains.get(symbol.id);
297
- if (cached !== undefined) {
298
- return cached;
299
- }
300
- const ids: string[] = [];
301
- const walked = new Set<number>();
302
- let at: TSSymbol | undefined = symbol;
303
- while (at !== undefined && !walked.has(at.id)) {
304
- walked.add(at.id);
305
- for (const handle of at.declarations) {
306
- if (!ownFiles.has(handle.path)) {
307
- continue;
308
- }
309
- const node = project.declarationAt(handle)?.node;
310
- const id =
311
- node === undefined
312
- ? undefined
313
- : held.declarations.get(nodeKey(node.getSourceFile(), node));
314
- if (id !== undefined && !ids.includes(id)) {
315
- ids.push(id);
316
- }
317
- }
318
- at = project.aliasStepOf(at);
319
- }
320
- chains.set(symbol.id, ids);
321
- return ids;
322
- };
291
+ const chains = aliasChains(project, held);
323
292
 
324
293
  unique.forEach((file, index) => {
325
294
  const module = modules[index];
@@ -329,8 +298,8 @@ function exportTables<Brand>(
329
298
  }
330
299
  const ids = new Set<string>();
331
300
  for (const exported of project.checker.getExportsOfModule(module)) {
332
- for (const id of chainOf(exported)) {
333
- ids.add(id);
301
+ for (const target of chains.chainOf(exported)) {
302
+ ids.add(target.id);
334
303
  }
335
304
  }
336
305
  tables.set(file.fileName, [...ids]);
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 compiler configuration of every project one run analyzes, one per
249
- * line, in discovery order, having read each project's diagnostics first.
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 configFile of projects) {
278
- out.write(`${configFile}\n`);
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/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. It is disposed once when the last
226
- * project has been visited, and the client is released after it, whether the visit
227
- * completed or threw.
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 projects = snapshot.getProjects().map((project) => visit(viewOf(project)));
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 ADDED
@@ -0,0 +1,224 @@
1
+ /**
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
+ * 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 { namesACaller, OUTSIDE, type Graph } from "./graph.ts";
10
+
11
+ /** The liveness relation that decided a declaration, as a finding spells it. */
12
+ export type Relation = "reference-counting" | "reachability";
13
+
14
+ /**
15
+ * The run's reference mode, decided once by the composition and read by every stage
16
+ * that counts references.
17
+ */
18
+ export interface Mode {
19
+ /**
20
+ * Counts no reference a test file made, under either relation. Every reference a root
21
+ * in a test file makes is such a reference, so nothing only a test reaches is live.
22
+ */
23
+ readonly production: boolean;
24
+ }
25
+
26
+ /** What one sweep runs over besides the graph. */
27
+ export interface SweepInput {
28
+ /**
29
+ * The declarations a matched suppression names. A mark holds its declaration live
30
+ * under both relations and seeds reachability through every reference it makes, so
31
+ * nothing only a marked declaration references is dead.
32
+ */
33
+ readonly marked: readonly string[];
34
+ readonly mode: Mode;
35
+ }
36
+
37
+ /** One dead declaration, the relation that found it, and the counts a kind reads. */
38
+ export interface Candidate {
39
+ readonly id: string;
40
+ /** The references a production file made to the declaration, whatever the mode counts. */
41
+ readonly productionRefs: number;
42
+ /** The references a test file made to it. */
43
+ readonly testRefs: number;
44
+ /**
45
+ * Reference counting where the mode counts no reference to the declaration, and
46
+ * reachability otherwise, so a declaration dead under both carries the stronger claim.
47
+ */
48
+ readonly relation: Relation;
49
+ /**
50
+ * Whether the test-of-dead-code rule admitted the declaration: a test declaration whose
51
+ * set of referenced production declarations is non-empty and wholly dead.
52
+ */
53
+ readonly testOfDeadCode: boolean;
54
+ }
55
+
56
+ /** One graph's answer: what each relation holds live, and what is dead. */
57
+ export interface Liveness {
58
+ /**
59
+ * Per declaration, the relations that hold it live, reference counting first. A
60
+ * declaration neither holds live has no entry.
61
+ */
62
+ readonly liveUnder: ReadonlyMap<string, readonly Relation[]>;
63
+ /** The dead declarations, in the order the inventory holds them. */
64
+ readonly candidates: readonly Candidate[];
65
+ }
66
+
67
+ /**
68
+ * Answers which declarations of one graph are dead under the input's mode.
69
+ *
70
+ * The order is the order the answers depend on: the callers and the marks are live
71
+ * before either relation runs, the candidate set is every judged declaration at least
72
+ * one relation does not hold live, and the tests of dead code join it.
73
+ */
74
+ export function sweep(graph: Graph, input: SweepInput): Liveness {
75
+ const marked = graph.symbols.map(() => false);
76
+ for (const id of input.marked) {
77
+ const at = graph.at(id);
78
+ if (at !== OUTSIDE) {
79
+ marked[at] = true;
80
+ }
81
+ }
82
+ // Every root and every mark seeds reachability. A root of a kind that names a caller,
83
+ // and a mark, are live under reference counting as well; a root that only supposes a
84
+ // caller is not.
85
+ const called = graph.symbols.map(() => false);
86
+ for (const root of graph.rooted) {
87
+ if (namesACaller(root.kind)) {
88
+ called[root.at] = true;
89
+ }
90
+ }
91
+
92
+ const counted = (at: number): number => {
93
+ const made = graph.made[at];
94
+ if (made === undefined) {
95
+ return 0;
96
+ }
97
+ return input.mode.production ? made.production : made.production + made.test;
98
+ };
99
+ const reached = reachable(graph, marked, input.mode);
100
+ const liveUnder = graph.symbols.map((_symbol, at): readonly Relation[] => {
101
+ const relations: Relation[] = [];
102
+ if (marked[at] === true || called[at] === true || counted(at) > 0) {
103
+ relations.push("reference-counting");
104
+ }
105
+ if (reached[at] === true) {
106
+ relations.push("reachability");
107
+ }
108
+ return relations;
109
+ });
110
+
111
+ const dead = graph.symbols.map(
112
+ (_symbol, at) => graph.subject[at] === true && (liveUnder[at]?.length ?? 0) < 2,
113
+ );
114
+ const testOfDeadCode = admitTestsOfDeadCode(graph, dead, marked);
115
+
116
+ const candidates: Candidate[] = [];
117
+ const live = new Map<string, readonly Relation[]>();
118
+ graph.symbols.forEach((symbol, at) => {
119
+ const relations = liveUnder[at] ?? [];
120
+ if (relations.length > 0) {
121
+ live.set(symbol.id, relations);
122
+ }
123
+ if (dead[at] !== true) {
124
+ return;
125
+ }
126
+ candidates.push({
127
+ id: symbol.id,
128
+ productionRefs: graph.made[at]?.production ?? 0,
129
+ testRefs: graph.made[at]?.test ?? 0,
130
+ relation: counted(at) === 0 ? "reference-counting" : "reachability",
131
+ testOfDeadCode: testOfDeadCode[at] === true,
132
+ });
133
+ });
134
+
135
+ return { liveUnder: live, candidates };
136
+ }
137
+
138
+ /**
139
+ * Per declaration, whether a seed reaches it. The two seeds reach through different
140
+ * reference sets: a marked declaration is live by a mechanism the analysis cannot see, so
141
+ * no mode withholds a reference it makes, while a root reaches through the references the
142
+ * mode counts. The marks run first, so what they reach is expanded under the wider rule.
143
+ */
144
+ function reachable(graph: Graph, marked: readonly boolean[], mode: Mode): readonly boolean[] {
145
+ const reached = graph.symbols.map(() => false);
146
+ const expanded = graph.symbols.map(() => false);
147
+ const queue: number[] = [];
148
+
149
+ // Reaching a file reaches its top level: the references written outside every
150
+ // declaration it holds. A reference that reads a file reads the module's namespace,
151
+ // which reaches what the module exports as well. An evaluation runs the top level
152
+ // and reads no export, and a root does not expand either: a file a runtime runs
153
+ // without reading its exports is a root by itself.
154
+ const enter = (at: number, expands: boolean): void => {
155
+ if (!reached[at]) {
156
+ reached[at] = true;
157
+ queue.push(at);
158
+ }
159
+ if (!expands || expanded[at] === true) {
160
+ return;
161
+ }
162
+ expanded[at] = true;
163
+ for (const exported of graph.exportsOf[at] ?? []) {
164
+ enter(exported, false);
165
+ }
166
+ };
167
+ const walk = (held: boolean): void => {
168
+ for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
169
+ for (const edge of graph.out[at] ?? []) {
170
+ if (!held && mode.production && edge.test) {
171
+ continue;
172
+ }
173
+ enter(edge.to, !edge.evaluation);
174
+ }
175
+ }
176
+ };
177
+
178
+ marked.forEach((mark, at) => {
179
+ if (mark) {
180
+ enter(at, false);
181
+ }
182
+ });
183
+ walk(true);
184
+ for (const root of graph.rooted) {
185
+ enter(root.at, false);
186
+ }
187
+ walk(false);
188
+ return reached;
189
+ }
190
+
191
+ /**
192
+ * Admits every test declaration whose set of referenced production declarations is
193
+ * non-empty and wholly dead, and returns which it admitted. The set is read from every
194
+ * reference the test makes, not the ones the mode counts: a production sweep counts none,
195
+ * which is what makes the targets dead. A test referencing one live target is never admitted.
196
+ */
197
+ function admitTestsOfDeadCode(
198
+ graph: Graph,
199
+ dead: boolean[],
200
+ marked: readonly boolean[],
201
+ ): readonly boolean[] {
202
+ const admitted = graph.symbols.map(() => false);
203
+ graph.symbols.forEach((_symbol, at) => {
204
+ if (graph.test[at] !== true || graph.subject[at] !== true || marked[at] === true) {
205
+ return;
206
+ }
207
+ let targets = 0;
208
+ let live = 0;
209
+ for (const edge of graph.out[at] ?? []) {
210
+ if (graph.test[edge.to] === true || graph.subject[edge.to] !== true) {
211
+ continue;
212
+ }
213
+ targets += 1;
214
+ if (dead[edge.to] !== true) {
215
+ live += 1;
216
+ }
217
+ }
218
+ if (targets > 0 && live === 0) {
219
+ dead[at] = true;
220
+ admitted[at] = true;
221
+ }
222
+ });
223
+ return admitted;
224
+ }
package/src/version.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * independently of the analyzer's own version, which is the one the host answers
5
5
  * from the package the analyzer was installed from.
6
6
  */
7
- export const CONTRACT_VERSION = "2.1.0";
7
+ export const CONTRACT_VERSION = "2.2.0";