@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/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. 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 CHANGED
@@ -1,17 +1,29 @@
1
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;
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 { componentsOf, type Component } from "./components.ts";
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 sweep's answer. */
58
- export interface SweepResult {
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, the tests of dead code join it, and the components
76
- * are computed over the set that results.
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): 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.
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, marked, input.mode);
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) => graph.subject[at] === true && (liveUnder[at]?.length ?? 0) < 2,
132
+ (_symbol, at) =>
133
+ graph.subject[at] === true && exempt[at] !== true && (liveUnder[at]?.length ?? 0) < 2,
117
134
  );
118
- const testOfDeadCode = admitTestsOfDeadCode(graph, dead, marked);
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
- liveUnder: live,
141
- candidates,
142
- components: componentsOf(graph, dead, testOfDeadCode),
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 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.
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, marked: readonly boolean[], mode: Mode): readonly boolean[] {
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
- // 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 => {
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 (!viaReference || expanded[at] === true) {
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 = (held: boolean): void => {
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 (!held && mode.production && edge.test) {
204
+ if (!holding && mode.production && edge.test) {
177
205
  continue;
178
206
  }
179
- enter(edge.to, true);
207
+ enter(edge.to, !edge.evaluation);
180
208
  }
181
209
  }
182
210
  };
183
211
 
184
- marked.forEach((mark, at) => {
185
- if (mark) {
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 admitted.
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
- marked: readonly boolean[],
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 || marked[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
@@ -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.2.0";
7
+ export const CONTRACT_VERSION = "3.0.0";