@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.
@@ -0,0 +1,156 @@
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
+ import { componentsOf } from "./components.js";
9
+ import { namesACaller, OUTSIDE } from "./graph.js";
10
+ /**
11
+ * Answers which declarations of one graph are dead under the input's mode.
12
+ *
13
+ * The order is the order the answers depend on: the callers and the marks are live
14
+ * before either relation runs, the candidate set is every judged declaration at least
15
+ * one relation does not hold live, the tests of dead code join it, and the components
16
+ * are computed over the set that results.
17
+ */
18
+ export function sweep(graph, input) {
19
+ const marked = graph.symbols.map(() => false);
20
+ for (const id of input.marked) {
21
+ const at = graph.at(id);
22
+ if (at !== OUTSIDE) {
23
+ marked[at] = true;
24
+ }
25
+ }
26
+ // Every root and every mark seeds reachability. A root of a kind that names a caller,
27
+ // and a mark, are live under reference counting as well; a root that only supposes a
28
+ // caller is not.
29
+ const called = graph.symbols.map(() => false);
30
+ for (const root of graph.rooted) {
31
+ if (namesACaller(root.kind)) {
32
+ called[root.at] = true;
33
+ }
34
+ }
35
+ const counted = (at) => {
36
+ const made = graph.made[at];
37
+ if (made === undefined) {
38
+ return 0;
39
+ }
40
+ return input.mode.production ? made.production : made.production + made.test;
41
+ };
42
+ const reached = reachable(graph, marked, input.mode);
43
+ const liveUnder = graph.symbols.map((_symbol, at) => {
44
+ const relations = [];
45
+ if (marked[at] === true || called[at] === true || counted(at) > 0) {
46
+ relations.push("reference-counting");
47
+ }
48
+ if (reached[at] === true) {
49
+ relations.push("reachability");
50
+ }
51
+ return relations;
52
+ });
53
+ const dead = graph.symbols.map((_symbol, at) => graph.subject[at] === true && (liveUnder[at]?.length ?? 0) < 2);
54
+ const testOfDeadCode = admitTestsOfDeadCode(graph, dead, marked);
55
+ const candidates = [];
56
+ const live = new Map();
57
+ graph.symbols.forEach((symbol, at) => {
58
+ const relations = liveUnder[at] ?? [];
59
+ if (relations.length > 0) {
60
+ live.set(symbol.id, relations);
61
+ }
62
+ if (dead[at] !== true) {
63
+ return;
64
+ }
65
+ candidates.push({
66
+ id: symbol.id,
67
+ productionRefs: graph.made[at]?.production ?? 0,
68
+ testRefs: graph.made[at]?.test ?? 0,
69
+ relation: counted(at) === 0 ? "reference-counting" : "reachability",
70
+ testOfDeadCode: testOfDeadCode[at] === true,
71
+ });
72
+ });
73
+ return {
74
+ liveUnder: live,
75
+ candidates,
76
+ components: componentsOf(graph, dead, testOfDeadCode),
77
+ };
78
+ }
79
+ /**
80
+ * Per declaration, whether a seed reaches it. The two seeds reach through different
81
+ * reference sets: a marked declaration is live by a mechanism the analysis cannot see, so
82
+ * no mode withholds a reference it makes, while a root reaches through the references the
83
+ * mode counts. The marks run first, so what they reach is expanded under the wider rule.
84
+ */
85
+ function reachable(graph, marked, mode) {
86
+ const reached = graph.symbols.map(() => false);
87
+ const expanded = graph.symbols.map(() => false);
88
+ const queue = [];
89
+ // A reference to a file is a reference to the module's namespace, which reaches what
90
+ // the module exports as well as its top level. A root does not expand: a file a
91
+ // runtime runs without reading its exports is a root by itself.
92
+ const enter = (at, viaReference) => {
93
+ if (!reached[at]) {
94
+ reached[at] = true;
95
+ queue.push(at);
96
+ }
97
+ if (!viaReference || expanded[at] === true) {
98
+ return;
99
+ }
100
+ expanded[at] = true;
101
+ for (const exported of graph.exportsOf[at] ?? []) {
102
+ enter(exported, false);
103
+ }
104
+ };
105
+ const walk = (held) => {
106
+ for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
107
+ for (const edge of graph.out[at] ?? []) {
108
+ if (!held && mode.production && edge.test) {
109
+ continue;
110
+ }
111
+ enter(edge.to, true);
112
+ }
113
+ }
114
+ };
115
+ marked.forEach((mark, at) => {
116
+ if (mark) {
117
+ enter(at, false);
118
+ }
119
+ });
120
+ walk(true);
121
+ for (const root of graph.rooted) {
122
+ enter(root.at, false);
123
+ }
124
+ walk(false);
125
+ return reached;
126
+ }
127
+ /**
128
+ * Admits every test declaration whose set of referenced production declarations is
129
+ * non-empty and wholly dead, and returns which it admitted. The set is read from every
130
+ * reference the test makes, not the ones the mode counts: a production sweep counts none,
131
+ * which is what makes the targets dead. A test referencing one live target is never admitted.
132
+ */
133
+ function admitTestsOfDeadCode(graph, dead, marked) {
134
+ const admitted = graph.symbols.map(() => false);
135
+ graph.symbols.forEach((_symbol, at) => {
136
+ if (graph.test[at] !== true || graph.subject[at] !== true || marked[at] === true) {
137
+ return;
138
+ }
139
+ let targets = 0;
140
+ let live = 0;
141
+ for (const edge of graph.out[at] ?? []) {
142
+ if (graph.test[edge.to] === true || graph.subject[edge.to] !== true) {
143
+ continue;
144
+ }
145
+ targets += 1;
146
+ if (dead[edge.to] !== true) {
147
+ live += 1;
148
+ }
149
+ }
150
+ if (targets > 0 && live === 0) {
151
+ dead[at] = true;
152
+ admitted[at] = true;
153
+ }
154
+ });
155
+ return admitted;
156
+ }
@@ -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";
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "2.3.0",
3
+ "version": "3.0.0",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "2.3.0",
3
+ "version": "3.0.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
package/src/analysis.ts CHANGED
@@ -12,14 +12,17 @@ import type { Diagnostic } from "@typescript/native/unstable/sync";
12
12
  import type { Config, Provenance } from "./config.ts";
13
13
  import { diagnosticErrors, discoverProjects, DiscoveryError } from "./discover.ts";
14
14
  import type { Finding } from "./finding.ts";
15
+ import { graphOf } from "./graph.ts";
15
16
  import type { Host } from "./host.ts";
16
- import { inventory } from "./inventory.ts";
17
+ import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
17
18
  import { readManifest } from "./manifest.ts";
18
19
  import { relativePath, resolvePath } from "./paths.ts";
19
20
  import { byPosition, type Position } from "./position.ts";
20
- import { roots, unmatchedEverywhere, unmatchedRoots, type RootKind } from "./roots.ts";
21
+ import { references } from "./references.ts";
22
+ import { roots, unmatchedEverywhere, unmatchedRoots, type RootKind, type Roots } from "./roots.ts";
21
23
  import type { Scope } from "./scope.ts";
22
- import { diagnosticsOf, runSession, type Engine } from "./session.ts";
24
+ import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
25
+ import { sweep, type SweepInput, type SweepResult } from "./sweep.ts";
23
26
 
24
27
  /** The setting whose source decides where a finding about a configured root sits. */
25
28
  const ROOTS_SETTING = "roots.patterns";
@@ -39,8 +42,8 @@ export interface RunRoot {
39
42
  /** The root set of one run, and the findings about the roots the configuration names. */
40
43
  export interface RunRoots {
41
44
  /**
42
- * The configurations the run analyzed: each compiler configuration file, by its
43
- * path below the target root, in discovery order.
45
+ * The configurations the run analyzed, each by the name discovery gives its
46
+ * project, in discovery order.
44
47
  */
45
48
  readonly configurations: readonly string[];
46
49
  /**
@@ -80,26 +83,31 @@ function compare(a: string, b: string): number {
80
83
  return a < b ? -1 : 1;
81
84
  }
82
85
 
86
+ /** What every stage after the root set reads of one project. */
87
+ interface ProjectRead {
88
+ readonly configuration: string;
89
+ readonly held: Inventory;
90
+ readonly rooted: Roots;
91
+ }
92
+
83
93
  /**
84
- * The root set of the run the scope and the configuration describe.
85
- *
86
- * Every project is read for errors before anything else, and a project carrying one
87
- * fails the run with no root set, as every verb that reads declarations does. A
88
- * configured string is a finding when it names nothing in every project, and the
89
- * finding sits in the document that declared the roots.
94
+ * Reads every project the scope discovers, in one snapshot: each is checked for errors
95
+ * first, then enumerated, then rooted, and `stage` reads the rest of what it needs from
96
+ * the project while its view is open. A project carrying an error fails the run with no
97
+ * answer, as every verb that reads declarations does.
90
98
  */
91
- export function runRoots(
99
+ function readProjects<Answer>(
92
100
  engine: Engine,
93
101
  host: Host,
94
102
  scope: Scope,
95
103
  config: Config,
96
- provenance: Provenance,
97
- ): RunRoots {
104
+ stage: <Brand>(project: ProjectView<Brand>, read: ProjectRead) => Answer,
105
+ ): readonly Answer[] {
98
106
  const targetRoot = scope.target.path;
99
- const discovered = discoverProjects(engine, host, scope);
100
- const manifest = readManifest(host, targetRoot);
107
+ const discovered = discoverProjects(engine, host, scope, config.analysis.configurations);
108
+ const ids = new Map(discovered.projects.map((project) => [project.configFile, project.id]));
101
109
  const options = {
102
- manifest,
110
+ manifest: readManifest(host, targetRoot),
103
111
  patterns: config.rootPatterns,
104
112
  entryFiles: config.ts.entryFiles,
105
113
  testFiles: config.ts.testFiles,
@@ -113,7 +121,12 @@ export function runRoots(
113
121
  return undefined;
114
122
  }
115
123
  const held = inventory(project, host, targetRoot);
116
- return { held, rooted: roots(project, held, targetRoot, options) };
124
+ const rooted = roots(project, held, targetRoot, options);
125
+ const configuration =
126
+ ids.get(project.configFile) ??
127
+ relativePath(targetRoot, project.configFile) ??
128
+ project.configFile;
129
+ return stage(project, { configuration, held, rooted });
117
130
  });
118
131
  if (failures.length > 0) {
119
132
  throw new DiscoveryError(
@@ -121,15 +134,29 @@ export function runRoots(
121
134
  failures,
122
135
  );
123
136
  }
137
+ return projects.filter((answer): answer is Answer => answer !== undefined);
138
+ }
139
+
140
+ /**
141
+ * The root set of the run the scope and the configuration describe.
142
+ *
143
+ * A configured string is a finding when it names nothing in every project, and the
144
+ * finding sits in the document that declared the roots.
145
+ */
146
+ export function runRoots(
147
+ engine: Engine,
148
+ host: Host,
149
+ scope: Scope,
150
+ config: Config,
151
+ provenance: Provenance,
152
+ ): RunRoots {
153
+ const targetRoot = scope.target.path;
154
+ const projects = readProjects(engine, host, scope, config, (_project, read) => read);
124
155
 
125
156
  const configurations: string[] = [];
126
157
  const merged = new Map<string, Omit<RunRoot, "configurations"> & { configurations: string[] }>();
127
158
  for (const answer of projects) {
128
- if (answer === undefined) {
129
- continue;
130
- }
131
- const configuration =
132
- relativePath(targetRoot, answer.rooted.configFile) ?? answer.rooted.configFile;
159
+ const configuration = answer.configuration;
133
160
  configurations.push(configuration);
134
161
  const symbols = new Map(answer.held.symbols.map((symbol) => [symbol.id, symbol]));
135
162
  for (const root of answer.rooted.liveUnderReachability) {
@@ -159,7 +186,7 @@ export function runRoots(
159
186
  );
160
187
  const unmatched = unmatchedEverywhere(
161
188
  config.rootPatterns,
162
- projects.flatMap((answer) => (answer === undefined ? [] : [answer.rooted])),
189
+ projects.map((answer) => answer.rooted),
163
190
  );
164
191
  return {
165
192
  configurations,
@@ -167,3 +194,43 @@ export function runRoots(
167
194
  findings: unmatchedRoots(unmatched, documentOf(host, provenance, ROOTS_SETTING, targetRoot)),
168
195
  };
169
196
  }
197
+
198
+ /** One configuration's sweep, beside the declarations it judged. */
199
+ export interface ConfigurationSweep {
200
+ /** The name discovery gives the configuration's project. */
201
+ readonly configuration: string;
202
+ readonly symbols: readonly InventorySymbol[];
203
+ readonly sweep: SweepResult;
204
+ }
205
+
206
+ /**
207
+ * Sweeps every project of the run the scope and the configuration describe, one
208
+ * configuration at a time, in discovery order. Each project's references are read
209
+ * against its own inventory with the configured test-file patterns, and its graph is
210
+ * swept under the input the caller decided once for the run.
211
+ */
212
+ export function runSweeps(
213
+ engine: Engine,
214
+ host: Host,
215
+ scope: Scope,
216
+ config: Config,
217
+ input: SweepInput,
218
+ ): readonly ConfigurationSweep[] {
219
+ const targetRoot = scope.target.path;
220
+ return readProjects(engine, host, scope, config, (project, read) => {
221
+ const resolved = references(project, read.held, targetRoot, {
222
+ testFiles: config.ts.testFiles,
223
+ });
224
+ const graph = graphOf(
225
+ read.held.symbols,
226
+ resolved.references,
227
+ read.rooted.liveUnderReachability,
228
+ resolved.testFilePaths,
229
+ );
230
+ return {
231
+ configuration: read.configuration,
232
+ symbols: read.held.symbols,
233
+ sweep: sweep(graph, input),
234
+ };
235
+ });
236
+ }