@cplieger/deadset-ts 3.0.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/dist/src/roots.js CHANGED
@@ -2,12 +2,13 @@
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
+ import { aliasChains } from "./alias-chain.js";
11
12
  import { REPOSITORY_DOCUMENT } from "./config.js";
12
13
  import { entryPoints } from "./entry-points.js";
13
14
  import { globExpression } from "./glob.js";
@@ -143,7 +144,10 @@ export function roots(project, held, targetRoot, options) {
143
144
  for (const point of entryPoints(project, files, options.testFiles)) {
144
145
  enter(point.file, point.rule, point.source, point.exports);
145
146
  }
146
- const exportsOf = exportTables(project, held, [...entered.filter((entry) => entry.exports).map((entry) => entry.file), ...published], files.byName);
147
+ const exportsOf = exportTables(project, held, [
148
+ ...entered.filter((entry) => entry.exports).map((entry) => entry.file),
149
+ ...published,
150
+ ]);
147
151
  for (const entry of entered) {
148
152
  const id = fileIds.get(entry.file.fileName);
149
153
  if (id === undefined) {
@@ -181,41 +185,14 @@ export function roots(project, held, targetRoot, options) {
181
185
  * its export table, star re-exports resolved into it. A file that is no module
182
186
  * exports nothing.
183
187
  */
184
- function exportTables(project, held, wanted, ownFiles) {
188
+ function exportTables(project, held, wanted) {
185
189
  const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
186
190
  const tables = new Map();
187
191
  if (unique.length === 0) {
188
192
  return tables;
189
193
  }
190
194
  const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
191
- const chains = new Map();
192
- const chainOf = (symbol) => {
193
- const cached = chains.get(symbol.id);
194
- if (cached !== undefined) {
195
- return cached;
196
- }
197
- const ids = [];
198
- const walked = new Set();
199
- let at = symbol;
200
- while (at !== undefined && !walked.has(at.id)) {
201
- walked.add(at.id);
202
- for (const handle of at.declarations) {
203
- if (!ownFiles.has(handle.path)) {
204
- continue;
205
- }
206
- const node = project.declarationAt(handle)?.node;
207
- const id = node === undefined
208
- ? undefined
209
- : held.declarations.get(nodeKey(node.getSourceFile(), node));
210
- if (id !== undefined && !ids.includes(id)) {
211
- ids.push(id);
212
- }
213
- }
214
- at = project.aliasStepOf(at);
215
- }
216
- chains.set(symbol.id, ids);
217
- return ids;
218
- };
195
+ const chains = aliasChains(project, held);
219
196
  unique.forEach((file, index) => {
220
197
  const module = modules[index];
221
198
  if (module === undefined) {
@@ -224,8 +201,8 @@ function exportTables(project, held, wanted, ownFiles) {
224
201
  }
225
202
  const ids = new Set();
226
203
  for (const exported of project.checker.getExportsOfModule(module)) {
227
- for (const id of chainOf(exported)) {
228
- ids.add(id);
204
+ for (const target of chains.chainOf(exported)) {
205
+ ids.add(target.id);
229
206
  }
230
207
  }
231
208
  tables.set(file.fileName, [...ids]);
@@ -1,5 +1,6 @@
1
1
  import { API, SymbolFlags } from "@typescript/native/unstable/sync";
2
2
  import { isIdentifier } from "@typescript/native/unstable/ast";
3
+ import { DiscoveryError } from "./discover.js";
3
4
  /**
4
5
  * Opens the compiler client the analysis reads through.
5
6
  *
@@ -81,19 +82,29 @@ export function diagnosticsOf(project) {
81
82
  }
82
83
  /**
83
84
  * Runs `visit` over every project the compiler configurations name, inside one
84
- * snapshot of one client.
85
+ * snapshot of one client, in the order they are named, and answers in that order.
85
86
  *
86
87
  * The snapshot is opened once with every configuration, so each becomes a project
87
88
  * with its own program and checker; it is never updated, because the analysis
88
- * reads and the tree does not change under it. It is disposed once when the last
89
- * project has been visited, and the client is released after it, whether the visit
90
- * completed or threw.
89
+ * reads and the tree does not change under it. The order the snapshot lists its
90
+ * projects in is the compiler's own, so each named configuration is looked up in it
91
+ * instead, and a configuration the snapshot opened no project for ends the run: the
92
+ * run would otherwise answer for fewer projects than it was asked to analyze. The
93
+ * snapshot is disposed once when the last project has been visited, and the client
94
+ * is released after it, whether the visit completed or threw.
91
95
  */
92
96
  export function runSession(engine, configFiles, visit) {
93
97
  try {
94
98
  const snapshot = engine.updateSnapshot(configFiles);
95
99
  try {
96
- const projects = snapshot.getProjects().map((project) => visit(viewOf(project)));
100
+ const opened = new Map(snapshot.getProjects().map((project) => [project.configFileName, project]));
101
+ const projects = configFiles.map((configFile) => {
102
+ const project = opened.get(configFile);
103
+ if (project === undefined) {
104
+ throw new DiscoveryError(`${configFile} opened no project`, []);
105
+ }
106
+ return visit(viewOf(project));
107
+ });
97
108
  return { projects, timing: engine.getTimingInfo() };
98
109
  }
99
110
  finally {
package/dist/src/sweep.js CHANGED
@@ -1,19 +1,17 @@
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
- import { componentsOf } from "./components.js";
9
8
  import { namesACaller, OUTSIDE } from "./graph.js";
10
9
  /**
11
10
  * Answers which declarations of one graph are dead under the input's mode.
12
11
  *
13
12
  * The order is the order the answers depend on: the callers and the marks are live
14
13
  * 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.
14
+ * one relation does not hold live, and the tests of dead code join it.
17
15
  */
18
16
  export function sweep(graph, input) {
19
17
  const marked = graph.symbols.map(() => false);
@@ -70,11 +68,7 @@ export function sweep(graph, input) {
70
68
  testOfDeadCode: testOfDeadCode[at] === true,
71
69
  });
72
70
  });
73
- return {
74
- liveUnder: live,
75
- candidates,
76
- components: componentsOf(graph, dead, testOfDeadCode),
77
- };
71
+ return { liveUnder: live, candidates };
78
72
  }
79
73
  /**
80
74
  * Per declaration, whether a seed reaches it. The two seeds reach through different
@@ -86,15 +80,17 @@ function reachable(graph, marked, mode) {
86
80
  const reached = graph.symbols.map(() => false);
87
81
  const expanded = graph.symbols.map(() => false);
88
82
  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) => {
83
+ // Reaching a file reaches its top level: the references written outside every
84
+ // declaration it holds. A reference that reads a file reads the module's namespace,
85
+ // which reaches what the module exports as well. An evaluation runs the top level
86
+ // and reads no export, and a root does not expand either: a file a runtime runs
87
+ // without reading its exports is a root by itself.
88
+ const enter = (at, expands) => {
93
89
  if (!reached[at]) {
94
90
  reached[at] = true;
95
91
  queue.push(at);
96
92
  }
97
- if (!viaReference || expanded[at] === true) {
93
+ if (!expands || expanded[at] === true) {
98
94
  return;
99
95
  }
100
96
  expanded[at] = true;
@@ -108,7 +104,7 @@ function reachable(graph, marked, mode) {
108
104
  if (!held && mode.production && edge.test) {
109
105
  continue;
110
106
  }
111
- enter(edge.to, true);
107
+ enter(edge.to, !edge.evaluation);
112
108
  }
113
109
  }
114
110
  };
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cplieger/deadset-ts",
3
- "version": "3.0.0",
3
+ "version": "3.1.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": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Deterministic dead-code analysis for TypeScript, class and type members included",
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Where an import or export chain leads in one project: from one symbol to each
3
+ * declaration of the target it is, and along the chain of aliases it heads to each
4
+ * declaration every link of that chain is.
5
+ */
6
+
7
+ import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
8
+ import { nodeKey, type Inventory } from "./inventory.ts";
9
+ import type { ProjectView } from "./session.ts";
10
+
11
+ /** One declaration a chain reaches, and whether a step along an alias reached it. */
12
+ export interface ChainTarget {
13
+ readonly id: string;
14
+ /** False for a declaration the head symbol itself is, true for one a link stands for. */
15
+ readonly stepped: boolean;
16
+ }
17
+
18
+ /** The chain walks over one project's aliases, each step asked of the compiler once. */
19
+ export interface AliasChains {
20
+ /**
21
+ * The declarations of the inventory one symbol is, each once. A declaration outside
22
+ * the project's own files belongs to another program and is not one of them.
23
+ */
24
+ declarationsOf(symbol: TSSymbol): readonly string[];
25
+ /**
26
+ * The declarations one symbol is, and those of every link of the alias chain it
27
+ * stands at the head of, each once and in chain order. A link the inventory does
28
+ * not declare is stepped over rather than named.
29
+ */
30
+ chainOf(symbol: TSSymbol): readonly ChainTarget[];
31
+ /**
32
+ * The declarations of the first link after `symbol` along its chain that the
33
+ * inventory declares, stepping over the links it does not, or none where no later
34
+ * link is declared.
35
+ */
36
+ nextDeclared(symbol: TSSymbol): readonly string[];
37
+ /** How many steps the compiler was asked for: one per alias, whatever the chains it heads. */
38
+ readonly steps: number;
39
+ }
40
+
41
+ /**
42
+ * The chain walks over one project, against the declarations of `held`, the same
43
+ * project's inventory.
44
+ *
45
+ * A declaration handle carries the file it is in, so whether it is one of the
46
+ * project's own files is answered before the handle is resolved: a declaration of a
47
+ * library file is never fetched to find out that the inventory does not hold it.
48
+ */
49
+ export function aliasChains<Brand>(project: ProjectView<Brand>, held: Inventory): AliasChains {
50
+ const ownFiles = new Set(project.ownSourceFiles().map((file) => file.fileName));
51
+ const steps = new Map<number, TSSymbol | undefined>();
52
+ const chains = new Map<number, readonly ChainTarget[]>();
53
+ let asked = 0;
54
+
55
+ const declarationsOf = (symbol: TSSymbol): string[] => {
56
+ const ids: string[] = [];
57
+ for (const handle of symbol.declarations) {
58
+ if (!ownFiles.has(handle.path)) {
59
+ continue;
60
+ }
61
+ const node = project.declarationAt(handle)?.node;
62
+ const id =
63
+ node === undefined ? undefined : held.declarations.get(nodeKey(node.getSourceFile(), node));
64
+ if (id !== undefined && !ids.includes(id)) {
65
+ ids.push(id);
66
+ }
67
+ }
68
+ return ids;
69
+ };
70
+
71
+ /**
72
+ * What one alias names, one link along. Only a symbol the binder flagged an alias is
73
+ * asked, which is the condition the step asserts on: a default export of an
74
+ * expression is written in an alias form and names nothing declared elsewhere, so its
75
+ * chain ends at itself.
76
+ */
77
+ const stepOf = (symbol: TSSymbol): TSSymbol | undefined => {
78
+ if ((symbol.flags & SymbolFlags.Alias) === 0) {
79
+ return undefined;
80
+ }
81
+ if (steps.has(symbol.id)) {
82
+ return steps.get(symbol.id);
83
+ }
84
+ const next = project.aliasStepOf(symbol);
85
+ asked += 1;
86
+ steps.set(symbol.id, next);
87
+ return next;
88
+ };
89
+
90
+ const chainOf = (symbol: TSSymbol): readonly ChainTarget[] => {
91
+ const cached = chains.get(symbol.id);
92
+ if (cached !== undefined) {
93
+ return cached;
94
+ }
95
+ const targets: ChainTarget[] = [];
96
+ const walked = new Set<number>();
97
+ let at: TSSymbol | undefined = symbol;
98
+ let stepped = false;
99
+ while (at !== undefined && !walked.has(at.id)) {
100
+ walked.add(at.id);
101
+ for (const id of declarationsOf(at)) {
102
+ if (!targets.some((target) => target.id === id)) {
103
+ targets.push({ id, stepped });
104
+ }
105
+ }
106
+ at = stepOf(at);
107
+ stepped = true;
108
+ }
109
+ chains.set(symbol.id, targets);
110
+ return targets;
111
+ };
112
+
113
+ const nextDeclared = (symbol: TSSymbol): readonly string[] => {
114
+ const walked = new Set<number>([symbol.id]);
115
+ let at = stepOf(symbol);
116
+ while (at !== undefined && !walked.has(at.id)) {
117
+ walked.add(at.id);
118
+ const ids = declarationsOf(at);
119
+ if (ids.length > 0) {
120
+ return ids;
121
+ }
122
+ at = stepOf(at);
123
+ }
124
+ return [];
125
+ };
126
+
127
+ return {
128
+ declarationsOf,
129
+ chainOf,
130
+ nextDeclared,
131
+ get steps() {
132
+ return asked;
133
+ },
134
+ };
135
+ }
package/src/analysis.ts CHANGED
@@ -12,17 +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";
16
15
  import type { Host } from "./host.ts";
17
- import { inventory, type Inventory, type InventorySymbol } from "./inventory.ts";
16
+ import { inventory, type Inventory } from "./inventory.ts";
18
17
  import { readManifest } from "./manifest.ts";
18
+ import { matrixOf, sweepMatrix, type Configured, type Matrix, type SweepResult } from "./matrix.ts";
19
19
  import { relativePath, resolvePath } from "./paths.ts";
20
20
  import { byPosition, type Position } from "./position.ts";
21
21
  import { references } from "./references.ts";
22
22
  import { roots, unmatchedEverywhere, unmatchedRoots, type RootKind, type Roots } from "./roots.ts";
23
23
  import type { Scope } from "./scope.ts";
24
24
  import { diagnosticsOf, runSession, type Engine, type ProjectView } from "./session.ts";
25
- import { sweep, type SweepInput, type SweepResult } from "./sweep.ts";
25
+ import type { SweepInput } from "./sweep.ts";
26
26
 
27
27
  /** The setting whose source decides where a finding about a configured root sits. */
28
28
  const ROOTS_SETTING = "roots.patterns";
@@ -43,7 +43,7 @@ export interface RunRoot {
43
43
  export interface RunRoots {
44
44
  /**
45
45
  * The configurations the run analyzed, each by the name discovery gives its
46
- * project, in discovery order.
46
+ * project, in discovery order: the order a declared matrix lists them in.
47
47
  */
48
48
  readonly configurations: readonly string[];
49
49
  /**
@@ -122,10 +122,7 @@ function readProjects<Answer>(
122
122
  }
123
123
  const held = inventory(project, host, targetRoot);
124
124
  const rooted = roots(project, held, targetRoot, options);
125
- const configuration =
126
- ids.get(project.configFile) ??
127
- relativePath(targetRoot, project.configFile) ??
128
- project.configFile;
125
+ const configuration = ids.get(project.configFile) ?? project.configFile;
129
126
  return stage(project, { configuration, held, rooted });
130
127
  });
131
128
  if (failures.length > 0) {
@@ -195,42 +192,39 @@ export function runRoots(
195
192
  };
196
193
  }
197
194
 
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[];
195
+ /** The run's sweep, beside the matrix it judged. */
196
+ export interface RunSweep {
197
+ readonly matrix: Matrix;
203
198
  readonly sweep: SweepResult;
204
199
  }
205
200
 
206
201
  /**
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.
202
+ * Sweeps the run the scope and the configuration describe. Each project's references
203
+ * are read against its own inventory with the configured test-file patterns while its
204
+ * view is open; the projects then merge into one matrix in discovery order, which is
205
+ * swept under the input the caller decided once for the run. A project that carries
206
+ * an error ends the run before any project is swept.
211
207
  */
212
- export function runSweeps(
208
+ export function runSweep(
213
209
  engine: Engine,
214
210
  host: Host,
215
211
  scope: Scope,
216
212
  config: Config,
217
213
  input: SweepInput,
218
- ): readonly ConfigurationSweep[] {
214
+ ): RunSweep {
219
215
  const targetRoot = scope.target.path;
220
- return readProjects(engine, host, scope, config, (project, read) => {
216
+ const configured = readProjects(engine, host, scope, config, (project, read): Configured => {
221
217
  const resolved = references(project, read.held, targetRoot, {
222
218
  testFiles: config.ts.testFiles,
223
219
  });
224
- const graph = graphOf(
225
- read.held.symbols,
226
- resolved.references,
227
- read.rooted.liveUnderReachability,
228
- resolved.testFilePaths,
229
- );
230
220
  return {
231
221
  configuration: read.configuration,
232
222
  symbols: read.held.symbols,
233
- sweep: sweep(graph, input),
223
+ references: resolved.references,
224
+ roots: read.rooted.liveUnderReachability,
225
+ testFiles: resolved.testFilePaths,
234
226
  };
235
227
  });
228
+ const matrix = matrixOf(configured);
229
+ return { matrix, sweep: sweepMatrix(matrix, input) };
236
230
  }
package/src/components.ts CHANGED
@@ -35,7 +35,7 @@ export interface Component {
35
35
  * neither and is counted by its own component.
36
36
  */
37
37
  readonly falls: readonly string[];
38
- /** The source lines the deletion removes, summed over {@link Component.falls}. */
38
+ /** The source lines the deletion removes: the distinct lines {@link Component.falls} spans. */
39
39
  readonly deletableLines: number;
40
40
  }
41
41
 
package/src/graph.ts CHANGED
@@ -72,6 +72,11 @@ export function namesACaller(kind: RootKind): boolean {
72
72
  export interface Edge {
73
73
  readonly to: number;
74
74
  readonly test: boolean;
75
+ /**
76
+ * Whether the reference evaluates the module whose file it names, which reaches the
77
+ * module's top level and none of what it exports.
78
+ */
79
+ readonly evaluation: boolean;
75
80
  }
76
81
 
77
82
  /** How many references one declaration carries, split by the classification of the file that made each. */
@@ -102,8 +107,8 @@ export interface Graph {
102
107
  readonly subject: readonly boolean[];
103
108
  /**
104
109
  * Per declaration, the declarations its export table names where it is a file, and
105
- * nothing for any other declaration. A reference to a file is a reference to the
106
- * module's namespace, which reaches everything the module exports.
110
+ * nothing for any other declaration. A reference that reads a file reads the module's
111
+ * namespace, which reaches everything the module exports.
107
112
  */
108
113
  readonly exportsOf: readonly (readonly number[])[];
109
114
  /** Every root naming a declaration the inventory holds, in the order the root set gives them. */
@@ -144,7 +149,11 @@ export function graphOf(
144
149
  counts.production += 1;
145
150
  }
146
151
  }
147
- out[at(reference.from)]?.push({ to, test: reference.test });
152
+ out[at(reference.from)]?.push({
153
+ to,
154
+ test: reference.test,
155
+ evaluation: reference.use === "evaluation",
156
+ });
148
157
  }
149
158
 
150
159
  const parent = symbols.map((symbol) => at(symbol.parent));