@cplieger/deadset-ts 8.0.0-dev.17 → 8.0.0-dev.19

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.
Files changed (86) hide show
  1. package/dist/src/analysis.js +30 -72
  2. package/dist/src/augmentations.js +1 -1
  3. package/dist/src/calls.js +267 -48
  4. package/dist/src/commands/held.js +499 -0
  5. package/dist/src/commands/read.js +16 -10
  6. package/dist/src/commands/shell.js +109 -15
  7. package/dist/src/commands/tools.js +1540 -149
  8. package/dist/src/conformance-record.js +1 -1
  9. package/dist/src/contract/contract.generated.js +9 -2
  10. package/dist/src/entries/conventions.js +7 -6
  11. package/dist/src/entries/test-runners.js +6 -40
  12. package/dist/src/entries/workflow-steps.js +202 -28
  13. package/dist/src/exempt.js +0 -26
  14. package/dist/src/exposure.js +400 -0
  15. package/dist/src/findings/completion.js +4 -17
  16. package/dist/src/findings/emitters.js +22 -33
  17. package/dist/src/findings/intra-function.js +3 -3
  18. package/dist/src/findings/non-code-artifacts.js +11 -5
  19. package/dist/src/findings/reads-and-writes.js +71 -15
  20. package/dist/src/findings/unused-declarations.js +2 -22
  21. package/dist/src/findings/visibility-narrowing.js +14 -20
  22. package/dist/src/inventory.js +18 -0
  23. package/dist/src/{components.js → liveness/components.js} +1 -1
  24. package/dist/src/{graph.js → liveness/graph.js} +38 -21
  25. package/dist/src/liveness/matrix.js +99 -0
  26. package/dist/src/liveness/sweep.js +558 -0
  27. package/dist/src/query.js +46 -0
  28. package/dist/src/references/sink.js +87 -1
  29. package/dist/src/references.js +1 -1
  30. package/dist/src/roots/manifest.js +47 -4
  31. package/dist/src/roots/published-reach.js +4 -4
  32. package/dist/src/roots/root-set.js +14 -16
  33. package/dist/src/text/named-files.js +257 -34
  34. package/dist/src/verbs/analyze.js +16 -1
  35. package/dist/src/verbs/explain.js +73 -152
  36. package/dist/src/verbs/print-retained.js +1 -1
  37. package/jsr.json +1 -1
  38. package/package.json +1 -1
  39. package/src/analysis.ts +54 -108
  40. package/src/augmentations.ts +1 -1
  41. package/src/calls.ts +326 -52
  42. package/src/commands/held.ts +632 -0
  43. package/src/commands/read.ts +26 -12
  44. package/src/commands/shell.ts +117 -15
  45. package/src/commands/tools.ts +1972 -202
  46. package/src/conformance-record.ts +1 -1
  47. package/src/contract/contract.generated.ts +9 -2
  48. package/src/dependencies.ts +1 -1
  49. package/src/entries/conventions.ts +8 -12
  50. package/src/entries/test-runners.ts +12 -45
  51. package/src/entries/workflow-steps.ts +227 -38
  52. package/src/exempt.ts +31 -46
  53. package/src/exposure.ts +492 -0
  54. package/src/file-facts.ts +5 -0
  55. package/src/finding.ts +1 -1
  56. package/src/findings/boundary.ts +7 -0
  57. package/src/findings/completion.ts +5 -19
  58. package/src/findings/emitters.ts +23 -41
  59. package/src/findings/interfaces.ts +3 -3
  60. package/src/findings/intra-function.ts +3 -3
  61. package/src/findings/non-code-artifacts.ts +16 -5
  62. package/src/findings/reads-and-writes.ts +99 -26
  63. package/src/findings/unused-declarations.ts +4 -28
  64. package/src/findings/visibility-narrowing.ts +25 -26
  65. package/src/interface-satisfaction.ts +1 -1
  66. package/src/inventory.ts +19 -0
  67. package/src/{components.ts → liveness/components.ts} +3 -3
  68. package/src/{graph.ts → liveness/graph.ts} +74 -26
  69. package/src/liveness/matrix.ts +162 -0
  70. package/src/liveness/sweep.ts +855 -0
  71. package/src/query.ts +81 -0
  72. package/src/references/sink.ts +101 -0
  73. package/src/references.ts +8 -1
  74. package/src/roots/manifest.ts +53 -8
  75. package/src/roots/published-reach.ts +9 -4
  76. package/src/roots/root-set.ts +18 -16
  77. package/src/stores.ts +2 -1
  78. package/src/tests/file-class.ts +19 -2
  79. package/src/text/named-files.ts +333 -36
  80. package/src/verbs/analyze.ts +16 -1
  81. package/src/verbs/explain.ts +81 -199
  82. package/src/verbs/print-retained.ts +1 -1
  83. package/dist/src/matrix.js +0 -275
  84. package/dist/src/sweep.js +0 -234
  85. package/src/matrix.ts +0 -466
  86. package/src/sweep.ts +0 -384
@@ -0,0 +1,558 @@
1
+ /**
2
+ * Which declarations of the run are dead, and under which liveness relation, answered once
3
+ * over the graph of every configuration. Reference counting holds a declaration live when any
4
+ * declaration references it or it is a root that names a caller; reachability holds it live
5
+ * when a path of references reaches it from a root. Reachability is settled by tiers, strongest
6
+ * first: a caller, an exemption or a live component file's markup, a suppression mark, and an
7
+ * unanswered checker question; each Contract rule "live only through" one is a view up to it.
8
+ */
9
+ import { exemptionsOf, isUnansweredRecord } from "../exempt.js";
10
+ import { edgeWithheldBy } from "../tests/file-class.js";
11
+ import { componentsOf } from "./components.js";
12
+ import { namesACaller, OUTSIDE } from "./graph.js";
13
+ /** The relations in the order a declaration's entry lists them. */
14
+ const RELATIONS = ["reference-counting", "reachability"];
15
+ /** The references to one declaration the mode counts: under `production` none test code made but those it classes as production. */
16
+ function countedRefs(made, mode) {
17
+ if (made === undefined) {
18
+ return 0;
19
+ }
20
+ if (!mode.production) {
21
+ return made.production + made.test;
22
+ }
23
+ return made.production + (mode.consumerTestsProduction === true ? made.consumerTest : 0);
24
+ }
25
+ /** The references to one declaration, split as the mode classes them, whatever it counts. */
26
+ function splitRefs(made, mode) {
27
+ const moved = mode.consumerTestsProduction === true ? (made?.consumerTest ?? 0) : 0;
28
+ return { production: (made?.production ?? 0) + moved, test: (made?.test ?? 0) - moved };
29
+ }
30
+ /**
31
+ * Per declaration, whether one pass reaches it, the step that first reached it, and the gated
32
+ * files that opened. Held seeds reach through every reference, a test file's included; roots
33
+ * and consumed references through those the mode counts, and a held seed met after a caller
34
+ * expands its declaration again. A read of a file reaches its exports; an evaluation, a root or
35
+ * a consumed reference to a file reaches its top level alone. The roots' first hops precede the
36
+ * consumed references, so a path is the shortest from the earliest root.
37
+ */
38
+ function reach(graph, seeds, mode, namespaces = true) {
39
+ const reached = graph.symbols.map(() => false);
40
+ const holding = graph.symbols.map(() => false);
41
+ const expanded = graph.symbols.map(() => [false, false]);
42
+ const by = graph.symbols.map(() => undefined);
43
+ const opened = new Set();
44
+ const queue = [];
45
+ const enter = (at, holds, expands, step) => {
46
+ if (!reached[at]) {
47
+ reached[at] = true;
48
+ by[at] = step;
49
+ const gate = seeds.gated.get(at);
50
+ if (gate !== undefined && seeds.counted(at)) {
51
+ opened.add(at);
52
+ for (const seed of gate) {
53
+ enter(seed.at, true, false, { k: "seed", origin: seed.origin });
54
+ }
55
+ }
56
+ if (!holds) {
57
+ queue.push({ at, holds });
58
+ }
59
+ }
60
+ if (holds && !holding[at]) {
61
+ holding[at] = true;
62
+ queue.push({ at, holds });
63
+ }
64
+ const mode = expanded[at];
65
+ if (!expands || mode === undefined || mode[holds ? 1 : 0] === true) {
66
+ return;
67
+ }
68
+ mode[holds ? 1 : 0] = true;
69
+ for (const exported of graph.exportsOf[at] ?? []) {
70
+ enter(exported, holds, false, step.k === "seed" ? step : { k: "export", from: step.from, edge: step.edge });
71
+ }
72
+ };
73
+ const follows = (edge, holds) => (seeds.guessed || !edge.guessed) && (holds || !edgeWithheldBy(mode, edge));
74
+ for (const seed of seeds.held) {
75
+ enter(seed.at, true, false, { k: "seed", origin: seed.origin });
76
+ }
77
+ for (const rooted of graph.rooted) {
78
+ enter(rooted.at, false, false, { k: "seed", origin: { k: "root", rooted } });
79
+ }
80
+ const firstHops = queue.length;
81
+ for (let head = 0;; head += 1) {
82
+ if (head === firstHops) {
83
+ for (const edge of graph.consumed) {
84
+ if (follows(edge, false)) {
85
+ enter(edge.to, false, namespaces && !edge.evaluation, {
86
+ k: "reference",
87
+ from: OUTSIDE,
88
+ edge,
89
+ });
90
+ }
91
+ }
92
+ }
93
+ const next = queue[head];
94
+ if (next === undefined) {
95
+ break;
96
+ }
97
+ for (const edge of graph.out[next.at] ?? []) {
98
+ if (follows(edge, next.holds)) {
99
+ enter(edge.to, next.holds, namespaces && !edge.evaluation, {
100
+ k: "reference",
101
+ from: next.at,
102
+ edge,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ return { reached, by, opened };
108
+ }
109
+ /**
110
+ * The directory a test runner reads a manual mock from, whose exports it puts in place of
111
+ * the module's: https://jestjs.io/docs/manual-mocks
112
+ */
113
+ const MANUAL_MOCKS = "__mocks__";
114
+ /** Whether a test runner calls or reads one export of a test file: a function, or any export of a manual mock. */
115
+ export function runnerCalls(symbol) {
116
+ return symbol.kind === "function" || symbol.position.path.split("/").includes(MANUAL_MOCKS);
117
+ }
118
+ /** The held seeds of the flagged declarations, each a suppression mark. */
119
+ function markSeeds(flags) {
120
+ return flags.flatMap((mark, at) => (mark ? [{ at, origin: { k: "mark", at } }] : []));
121
+ }
122
+ /** Per declaration of the graph, whether one of the identifiers names it. */
123
+ function flagged(graph, ids) {
124
+ const flags = graph.symbols.map(() => false);
125
+ for (const id of ids) {
126
+ const at = graph.at(id);
127
+ if (at !== OUTSIDE) {
128
+ flags[at] = true;
129
+ }
130
+ }
131
+ return flags;
132
+ }
133
+ /**
134
+ * Per declaration of the graph, whether the run that counts test references reaches it.
135
+ * That run's roots are the graph's own and, of each test file, its top level and every
136
+ * function it exports, which a test runner may call, and every export of a manual mock,
137
+ * which the runner reads; any other export of a test file, and test-support code, which
138
+ * runs where a test imports it, are judged as any file's are. `held` seeds it as a mark
139
+ * seeds a sweep; with `namespaces` false a file read reaches none of the file's exports.
140
+ */
141
+ export function reachedWithTheTests(graph, held, namespaces = true) {
142
+ const tests = graph.symbols.flatMap((symbol, at) => {
143
+ const exported = symbol.exported && graph.symbols[graph.parent[at] ?? OUTSIDE]?.kind === "file";
144
+ const called = exported && runnerCalls(symbol);
145
+ return graph.test[at] === true &&
146
+ graph.support[at] !== true &&
147
+ (symbol.kind === "file" || called)
148
+ ? [{ at, kind: "test-runner", source: symbol.position.path }]
149
+ : [];
150
+ });
151
+ return reach({ ...graph, rooted: [...graph.rooted, ...tests] }, {
152
+ held: markSeeds(flagged(graph, held)),
153
+ gated: new Map(),
154
+ guessed: true,
155
+ counted: () => true,
156
+ }, { production: false }, namespaces).reached;
157
+ }
158
+ /** One sweep's state over the matrix. */
159
+ class Sweeping {
160
+ graph;
161
+ policy;
162
+ marked;
163
+ called;
164
+ unseen;
165
+ constructor(graph, policy) {
166
+ this.graph = graph;
167
+ this.policy = policy;
168
+ this.marked = flagged(graph, policy.marked);
169
+ const called = graph.symbols.map(() => false);
170
+ const unseen = graph.symbols.map(() => false);
171
+ for (const root of graph.rooted) {
172
+ if (namesACaller(root.kind)) {
173
+ called[root.at] = true;
174
+ }
175
+ else if (policy.unseenCallers && root.kind === "published-api") {
176
+ unseen[root.at] = true;
177
+ }
178
+ }
179
+ this.called = called;
180
+ this.unseen = unseen;
181
+ }
182
+ /** The mode's count of one declaration's references, those only a name answered left out unless `guessed`. */
183
+ counted(at, guessed) {
184
+ const all = countedRefs(this.graph.made[at], this.policy.mode);
185
+ return guessed ? all : all - countedRefs(this.graph.guessedMade[at], this.policy.mode);
186
+ }
187
+ /** Whether reference counting holds one declaration live in a view. */
188
+ countLive(at, view) {
189
+ return ((view.marks && this.marked[at] === true) ||
190
+ this.called[at] === true ||
191
+ this.counted(at, view.guessed) > 0);
192
+ }
193
+ /** What one view reaches: its records and marks held, its gates open once their file is live. */
194
+ reachOf(view, mode = this.policy.mode) {
195
+ return reach(this.graph, {
196
+ held: [...view.records, ...(view.marks ? markSeeds(this.marked) : [])],
197
+ gated: view.gated,
198
+ guessed: view.guessed,
199
+ counted: (at) => this.countLive(at, view),
200
+ }, mode);
201
+ }
202
+ /**
203
+ * Judges every declaration under one view: the relations, then the candidates, which no
204
+ * record, opened gate or `kept` flag holds back, then the tests of dead code.
205
+ */
206
+ judge(view, reached, kept = []) {
207
+ const { graph } = this;
208
+ const exempt = graph.symbols.map(() => false);
209
+ for (const seed of [
210
+ ...view.records,
211
+ ...[...reached.opened].flatMap((file) => view.gated.get(file) ?? []),
212
+ ]) {
213
+ exempt[seed.at] = true;
214
+ }
215
+ const live = graph.symbols.map((_symbol, at) => [
216
+ ...(this.countLive(at, view) ? ["reference-counting"] : []),
217
+ ...(reached.reached[at] === true ? ["reachability"] : []),
218
+ ]);
219
+ const dead = graph.symbols.map((_symbol, at) => graph.subject[at] === true &&
220
+ exempt[at] !== true &&
221
+ kept[at] !== true &&
222
+ (live[at]?.length ?? 0) < RELATIONS.length);
223
+ const held = exempt.map((one, at) => one || (view.marks && this.marked[at] === true));
224
+ const testOfDeadCode = this.admit(dead, held);
225
+ const relation = graph.symbols.map((_symbol, at) => this.counted(at, view.guessed) === 0 ? "reference-counting" : "reachability");
226
+ return { live, dead, testOfDeadCode, relation };
227
+ }
228
+ /**
229
+ * Admits every test declaration whose referenced production declarations, read from every
230
+ * reference the test makes, are non-empty and all dead. An ambient declaration is no
231
+ * production declaration here, and a published one outside programs may call counts as
232
+ * live. A support declaration a test with a live target also references is a live target of
233
+ * every test referencing it. A held test is never admitted.
234
+ */
235
+ admit(dead, held) {
236
+ const { graph } = this;
237
+ const testCode = (at) => graph.test[at] === true || graph.support[at] === true;
238
+ const judged = graph.symbols.map((_symbol, at) => {
239
+ let targets = 0;
240
+ let live = 0;
241
+ if (testCode(at) && graph.subject[at] === true && held[at] !== true) {
242
+ for (const edge of graph.out[at] ?? []) {
243
+ if (testCode(edge.to) ||
244
+ graph.subject[edge.to] !== true ||
245
+ graph.ambient[edge.to] === true) {
246
+ continue;
247
+ }
248
+ targets += 1;
249
+ if (dead[edge.to] !== true || this.unseen[edge.to] === true) {
250
+ live += 1;
251
+ }
252
+ }
253
+ }
254
+ return { targets, live };
255
+ });
256
+ const shared = new Set();
257
+ judged.forEach(({ live }, at) => {
258
+ if (live > 0) {
259
+ for (const edge of graph.out[at] ?? []) {
260
+ if (graph.support[edge.to] === true) {
261
+ shared.add(edge.to);
262
+ }
263
+ }
264
+ }
265
+ });
266
+ const admitted = graph.symbols.map(() => false);
267
+ judged.forEach(({ targets, live }, at) => {
268
+ const sharing = (graph.out[at] ?? []).some((edge) => shared.has(edge.to) && edge.to !== at);
269
+ if (targets > 0 && live === 0 && !sharing) {
270
+ dead[at] = true;
271
+ admitted[at] = true;
272
+ }
273
+ });
274
+ return admitted;
275
+ }
276
+ }
277
+ /** The path one pass reached a declaration by, from the step it recorded back to its origin. */
278
+ function pathIn(by, at) {
279
+ const hops = [];
280
+ let cursor = at;
281
+ for (let step = by[cursor]; step !== undefined; step = by[cursor]) {
282
+ if (step.k === "seed") {
283
+ return { origin: step.origin, hops };
284
+ }
285
+ hops.unshift(step.k === "reference"
286
+ ? { k: "reference", edge: step.edge }
287
+ : { k: "export", edge: step.edge, to: cursor });
288
+ if (step.from === OUTSIDE) {
289
+ return { origin: { k: "consumed", edge: step.edge }, hops };
290
+ }
291
+ cursor = step.from;
292
+ }
293
+ return undefined;
294
+ }
295
+ /**
296
+ * Per declaration of the union, whether one of `seeds` is it or reaches it through `dead`
297
+ * declarations.
298
+ */
299
+ function testUsedReach(graph, seeds, dead) {
300
+ // A file is no candidate, and its top level is what reaches what it alone calls.
301
+ const reached = seeds.map((seed, at) => seed && dead[at] === true);
302
+ const queue = seeds.flatMap((one, at) => (one ? [at] : []));
303
+ for (let at = queue.pop(); at !== undefined; at = queue.pop()) {
304
+ for (const edge of graph.out[at] ?? []) {
305
+ if (dead[edge.to] === true && reached[edge.to] !== true) {
306
+ reached[edge.to] = true;
307
+ queue.push(edge.to);
308
+ }
309
+ }
310
+ }
311
+ return reached;
312
+ }
313
+ /** Per declaration, whether test code `reached` holds and `admitted` rejects references it. */
314
+ function keptByTests(graph, reached, admitted) {
315
+ const kept = graph.symbols.map(() => false);
316
+ graph.out.forEach((edges, from) => {
317
+ const testCode = graph.test[from] === true || graph.support[from] === true;
318
+ if (!testCode || reached[from] !== true || admitted[from] === true) {
319
+ return;
320
+ }
321
+ for (const edge of edges) {
322
+ if (edge.test) {
323
+ kept[edge.to] = true;
324
+ }
325
+ }
326
+ });
327
+ return kept;
328
+ }
329
+ /**
330
+ * The dead components of one sweep over the union, with the file declarations at `files`
331
+ * dead as well: a file that is deleted whole takes the declarations it holds and every
332
+ * dead declaration only its top level references with it.
333
+ */
334
+ export function deadComponents(matrix, result, files) {
335
+ const union = matrix.union;
336
+ const byId = new Map(result.candidates.map((candidate) => [candidate.id, candidate]));
337
+ const candidateAt = (at) => byId.get(union.symbols[at]?.id ?? "");
338
+ const positional = {
339
+ dependents: union.symbols.map((symbol) => (matrix.dependents.get(symbol.id) ?? [])
340
+ .map((id) => union.at(id))
341
+ .filter((at) => at !== OUTSIDE)),
342
+ live: union.symbols.map((symbol) => result.liveUnder.get(symbol.id)?.includes("reachability") === true),
343
+ };
344
+ return componentsOf(union, union.symbols.map((_symbol, at) => {
345
+ const candidate = candidateAt(at);
346
+ return files.has(at) || (candidate !== undefined && candidate.heldByTests === undefined);
347
+ }), union.symbols.map((_symbol, at) => candidateAt(at)?.testOfDeadCode === true), union.symbols.map((_symbol, at) => candidateAt(at)?.unreferencedTest === true), positional);
348
+ }
349
+ /** The held seeds of the policy's records, by how they hold. */
350
+ function seedsOf(graph, policy) {
351
+ const files = new Map(graph.symbols.flatMap((symbol, at) => symbol.kind === "file" ? [[symbol.position.path, at]] : []));
352
+ const records = [];
353
+ const unanswered = [];
354
+ const gated = new Map();
355
+ const gate = (path, seed) => {
356
+ const file = files.get(path);
357
+ if (file !== undefined) {
358
+ gated.set(file, [...(gated.get(file) ?? []), seed]);
359
+ }
360
+ };
361
+ for (const record of policy.exempt) {
362
+ const at = graph.at(record.id);
363
+ if (at === OUTSIDE) {
364
+ continue;
365
+ }
366
+ const seed = { at, origin: { k: "exemption", record } };
367
+ if (record.whileLive !== undefined) {
368
+ gate(record.whileLive, seed);
369
+ }
370
+ else if (isUnansweredRecord(record)) {
371
+ unanswered.push(seed);
372
+ }
373
+ else {
374
+ records.push(seed);
375
+ }
376
+ }
377
+ for (const id of policy.unanswered) {
378
+ const at = graph.at(id);
379
+ if (at !== OUTSIDE) {
380
+ unanswered.push({ at, origin: { k: "unanswered", at } });
381
+ }
382
+ }
383
+ return { records, unanswered, gated };
384
+ }
385
+ /**
386
+ * Answers which declarations of the run are dead under the policy, once over the union of
387
+ * every configuration's references and roots, so one configuration's code keeps a declaration
388
+ * live in the run (matrix.4, matrix.5). Where a question went unanswered, the view without the
389
+ * guessed tier decides what the gap holds: what it judges dead and the full view judges live,
390
+ * or dead by another rule or test-of-dead-code answer; those are no candidates. A record held
391
+ * a declaration back where the view without any record judges it dead.
392
+ */
393
+ export function sweep(matrix, policy) {
394
+ const graph = matrix.union;
395
+ const sweeping = new Sweeping(graph, policy);
396
+ const { records, unanswered, gated } = seedsOf(graph, policy);
397
+ const full = { records: [...records, ...unanswered], gated, marks: true, guessed: true };
398
+ const reached = sweeping.reachOf(full);
399
+ const openedSeeds = [...reached.opened].flatMap((file) => gated.get(file) ?? []);
400
+ const held = new Set(policy.unanswered);
401
+ if (held.size > 0) {
402
+ for (const reference of graph.references) {
403
+ if (reference.guessed === true) {
404
+ held.add(reference.from);
405
+ }
406
+ }
407
+ const fullJudged = sweeping.judge(full, reached);
408
+ const bare = {
409
+ records: [...records, ...openedSeeds],
410
+ gated: new Map(),
411
+ marks: true,
412
+ guessed: false,
413
+ };
414
+ const bareJudged = sweeping.judge(bare, sweeping.reachOf(bare));
415
+ graph.symbols.forEach((symbol, at) => {
416
+ if (bareJudged.dead[at] === true &&
417
+ (fullJudged.dead[at] !== true ||
418
+ fullJudged.testOfDeadCode[at] !== bareJudged.testOfDeadCode[at] ||
419
+ fullJudged.relation[at] !== bareJudged.relation[at])) {
420
+ held.add(symbol.id);
421
+ }
422
+ });
423
+ }
424
+ const judged = sweeping.judge(full, reached, flagged(graph, held));
425
+ const openedPaths = new Set([...reached.opened].flatMap((file) => graph.symbols[file]?.position.path ?? []));
426
+ const exempt = exemptionsOf(policy.exempt.flatMap(({ whileLive, ...record }) => whileLive === undefined || openedPaths.has(whileLive) ? [record] : []));
427
+ // The markup source makes each top-level binding a use of the file's (program.20), so a
428
+ // live file's bindings are live; the sweep answers which they are for the findings.
429
+ const components = new Set(policy.componentFiles);
430
+ const bindings = graph.symbols.flatMap((symbol, at) => {
431
+ const file = graph.parent[at] ?? OUTSIDE;
432
+ const holder = graph.symbols[file];
433
+ return holder?.kind === "file" &&
434
+ components.has(holder.position.path) &&
435
+ (judged.live[file]?.length ?? 0) === RELATIONS.length
436
+ ? [symbol.id]
437
+ : [];
438
+ });
439
+ const heldIds = [
440
+ ...policy.marked,
441
+ ...exempt.map((record) => record.id),
442
+ ...policy.unanswered,
443
+ ...bindings,
444
+ ];
445
+ const testReached = reachedWithTheTests(graph, heldIds);
446
+ // A test uses what it names: a file a test reads through a star re-export or a
447
+ // namespace keeps the exports nothing names at its own class.
448
+ const testNamed = reachedWithTheTests(graph, heldIds, false);
449
+ const kept = keptByTests(graph, testReached, judged.testOfDeadCode);
450
+ const testUsed = testUsedReach(graph, graph.symbols.map((_symbol, at) => matrix.testUsed[at] === true && testNamed[at] === true), judged.dead);
451
+ const heldBy = graph.symbols.map(() => undefined);
452
+ const candidates = [];
453
+ graph.symbols.forEach((symbol, at) => {
454
+ if (judged.dead[at] !== true) {
455
+ return;
456
+ }
457
+ const admitted = judged.testOfDeadCode[at] === true;
458
+ const split = splitRefs(graph.made[at], policy.mode);
459
+ const heldByTests = heldBy[graph.parent[at] ?? OUTSIDE] ??
460
+ (graph.support[at] === true && split.test > 0 && !admitted ? symbol.id : undefined);
461
+ heldBy[at] = heldByTests;
462
+ const unreferenced = graph.test[at] === true && !admitted && testReached[at] !== true;
463
+ candidates.push({
464
+ id: symbol.id,
465
+ productionRefs: split.production,
466
+ testRefs: split.test,
467
+ // The run that judges a test-file declaration counts its test references.
468
+ relation: unreferenced
469
+ ? split.production + split.test === 0
470
+ ? "reference-counting"
471
+ : "reachability"
472
+ : (judged.relation[at] ?? "reachability"),
473
+ testOfDeadCode: admitted,
474
+ configurations: matrix.heldIn[at] ?? [],
475
+ heldByTests,
476
+ unreferencedTest: unreferenced,
477
+ testUsed: testUsed[at] === true,
478
+ keptByTests: kept[at] === true,
479
+ });
480
+ });
481
+ const liveUnder = new Map(graph.symbols.flatMap((symbol, at) => {
482
+ const relations = judged.live[at] ?? [];
483
+ return relations.length === 0 ? [] : [[symbol.id, relations]];
484
+ }));
485
+ let retained = [];
486
+ if (exempt.length > 0) {
487
+ const without = { records: [], gated: new Map(), marks: true, guessed: true };
488
+ const bare = sweeping.judge(without, sweeping.reachOf(without));
489
+ const holding = new Map();
490
+ for (const record of exempt) {
491
+ const at = graph.at(record.id);
492
+ if (bare.dead[at] === true || policy.writeOnly.has(record.id)) {
493
+ holding.set(record.id, [...(holding.get(record.id) ?? []), record]);
494
+ }
495
+ }
496
+ retained = graph.symbols.flatMap((symbol) => holding.get(symbol.id) ?? []);
497
+ }
498
+ const tiers = [
499
+ { records: [], gated: new Map(), marks: false, guessed: false },
500
+ { records, gated, marks: false, guessed: false },
501
+ { records, gated, marks: true, guessed: false },
502
+ full,
503
+ ];
504
+ const passes = [];
505
+ const path = (id) => {
506
+ const at = graph.at(id);
507
+ if (at === OUTSIDE) {
508
+ return undefined;
509
+ }
510
+ for (const [place, view] of tiers.entries()) {
511
+ const pass = (passes[place] ??=
512
+ place === tiers.length - 1 ? reached : sweeping.reachOf(view));
513
+ if (pass.reached[at] === true) {
514
+ return pathIn(pass.by, at);
515
+ }
516
+ }
517
+ // A production run counts no test reference, so what only tests reach is shown by them.
518
+ if (!policy.mode.production) {
519
+ return undefined;
520
+ }
521
+ const counting = (passes[tiers.length] ??= sweeping.reachOf(tiers[0] ?? full, {
522
+ production: false,
523
+ }));
524
+ return counting.reached[at] === true ? pathIn(counting.by, at) : undefined;
525
+ };
526
+ const sweepAnswer = { liveUnder, candidates };
527
+ return {
528
+ ...sweepAnswer,
529
+ components: deadComponents(matrix, sweepAnswer, new Set()),
530
+ exempt,
531
+ retained,
532
+ held: [...held],
533
+ bindings,
534
+ carried: carriedOf(graph, policy.mode),
535
+ path,
536
+ };
537
+ }
538
+ /** {@link Sweep.carried}: a declaration one counted reference without a carrier names carries nothing. */
539
+ function carriedOf(graph, mode) {
540
+ const carried = new Map();
541
+ const free = new Set();
542
+ for (const edge of [...graph.out.flat(), ...graph.consumed]) {
543
+ if (edgeWithheldBy(mode, edge) || free.has(edge.to)) {
544
+ continue;
545
+ }
546
+ const id = graph.symbols[edge.to]?.id ?? "";
547
+ const chain = graph.references[edge.reference]?.carriers ?? [];
548
+ const known = carried.get(id) ?? [];
549
+ if (chain.length === 0) {
550
+ free.add(edge.to);
551
+ carried.delete(id);
552
+ }
553
+ else if (!known.some((one) => one.join("\u0000") === chain.join("\u0000"))) {
554
+ carried.set(id, [...known, chain]);
555
+ }
556
+ }
557
+ return carried;
558
+ }
package/dist/src/query.js CHANGED
@@ -88,6 +88,36 @@ export function propertyTables(queries) {
88
88
  },
89
89
  };
90
90
  }
91
+ /** How to read the types one type is spelled from, as {@link Queries.partsOf} names them. */
92
+ function partQuestion(type) {
93
+ if (type.isTypeParameter()) {
94
+ return () => [type.getConstraint(), type.getDefault()];
95
+ }
96
+ if (type.isConditionalType()) {
97
+ return () => [
98
+ type.getCheckType(),
99
+ type.getExtendsType(),
100
+ type.getTrueType(),
101
+ type.getFalseType(),
102
+ ];
103
+ }
104
+ if (type.isMappedType()) {
105
+ return () => [type.getConstraintType(), type.getNameType(), type.getTemplateType()];
106
+ }
107
+ if (type.isIndexedAccessType()) {
108
+ return () => [type.getObjectType(), type.getIndexType()];
109
+ }
110
+ if (type.isIndexType() || type.isStringMappingType()) {
111
+ return () => [type.getTarget()];
112
+ }
113
+ if (type.isTemplateLiteralType()) {
114
+ return () => type.getTypes();
115
+ }
116
+ if (type.isSubstitutionType()) {
117
+ return () => [type.getBaseType(), type.getConstraint()];
118
+ }
119
+ return undefined;
120
+ }
91
121
  /**
92
122
  * The guarded questions over one project's checker. `configFile` names the project
93
123
  * in the log, `batch` sends many questions in one round trip, `log` is the run's,
@@ -217,7 +247,23 @@ export function queriesOf(checker, configFile, batch, log, ask = ASK_DIRECTLY) {
217
247
  indexedAccessOf: (type) => type.isIndexedAccessType()
218
248
  ? one("getObjectType", () => typeLocation(type), () => ({ object: type.getObjectType(), index: type.getIndexType() }))
219
249
  : undefined,
250
+ partsOf: (type) => {
251
+ const parts = partQuestion(type);
252
+ return parts === undefined
253
+ ? []
254
+ : one("getParts", () => typeLocation(type), () => parts().filter((part) => part !== undefined));
255
+ },
256
+ signaturePartsOf: (signature) => one("getSignatureParts", () => `signature ${String(signature.id)}`, () => {
257
+ const self = signature.thisParameter === undefined ? undefined : signature.getThisParameter();
258
+ return [
259
+ ...((signature.typeParameters ?? []).length === 0 ? [] : signature.getTypeParameters()),
260
+ self === undefined ? undefined : checker.getTypeOfSymbol(self),
261
+ checker.getTypePredicateOfSignature(signature)?.type,
262
+ ].filter((part) => part !== undefined);
263
+ }),
220
264
  symbolOfType: (type) => one("getSymbol", () => typeLocation(type), () => type.getSymbol()),
265
+ aliasSymbolOf: (type) => one("getAliasSymbol", () => typeLocation(type), () => type.getAliasSymbol()),
266
+ aliasArgumentsOf: (type) => one("getAliasTypeArguments", () => typeLocation(type), () => type.getAliasTypeArguments()),
221
267
  signaturesOf: (type, kind) => one("getSignaturesOfType", () => typeLocation(type), () => checker.getSignaturesOfType(type, kind)),
222
268
  resolvedSignature: (node) => one("getResolvedSignature", () => nodeLocation(node), () => checker.getResolvedSignature(node)),
223
269
  resolvedSignatures: (nodes, cap = DEFAULT_BATCH_CAP) => each("getResolvedSignature", nodes, cap, nodeLocation, (node) => checker.getResolvedSignature.gen(node), (node) => checker.getResolvedSignature(node)),