@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/matrix.ts ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * One run's projects as one matrix: every declaration any project holds, keyed by its
3
+ * position and carrying the configurations that hold it, swept once per configuration
4
+ * and intersected, with the dead components computed once over every configuration.
5
+ */
6
+
7
+ import { componentsOf, type Component } from "./components.ts";
8
+ import { graphOf, type Graph } from "./graph.ts";
9
+ import type { InventorySymbol } from "./inventory.ts";
10
+ import { byPosition } from "./position.ts";
11
+ import type { Reference } from "./references.ts";
12
+ import type { Root } from "./roots.ts";
13
+ import { sweep, type Candidate, type Liveness, type Relation, type SweepInput } from "./sweep.ts";
14
+
15
+ /** What the passes answered for one configuration of the run. */
16
+ export interface Configured {
17
+ /** The name discovery gives the configuration's project. */
18
+ readonly configuration: string;
19
+ readonly symbols: readonly InventorySymbol[];
20
+ readonly references: readonly Reference[];
21
+ readonly roots: readonly Root[];
22
+ /** The paths the reference pass classified as test files. */
23
+ readonly testFiles: readonly string[];
24
+ }
25
+
26
+ /** The run's configurations, indexed for the sweep. */
27
+ export interface Matrix {
28
+ /** The configurations, in the run's order. */
29
+ readonly configurations: readonly string[];
30
+ /** Per configuration, in the same order, the graph of what that configuration holds. */
31
+ readonly graphs: readonly Graph[];
32
+ /**
33
+ * The graph over every configuration at once: every declaration any configuration
34
+ * holds, once per position and in site order, and every reference any configuration
35
+ * makes, a reference several configurations make at one position counted once.
36
+ */
37
+ readonly union: Graph;
38
+ /**
39
+ * Per declaration of {@link Matrix.union}, at its position there, the configurations
40
+ * whose project holds it, in the run's order.
41
+ */
42
+ readonly heldIn: readonly (readonly string[])[];
43
+ }
44
+
45
+ /** One declaration dead in every configuration that holds it. */
46
+ export interface MatrixCandidate extends Candidate {
47
+ /** The configurations the declaration is dead in, which are every one that holds it. */
48
+ readonly configurations: readonly string[];
49
+ }
50
+
51
+ /** One run's sweep. */
52
+ export interface SweepResult {
53
+ /**
54
+ * Per declaration, the relations that hold it live in at least one configuration,
55
+ * reference counting first. A declaration no relation holds live anywhere has no entry.
56
+ */
57
+ readonly liveUnder: ReadonlyMap<string, readonly Relation[]>;
58
+ /** The declarations dead in every configuration that holds them, in site order. */
59
+ readonly candidates: readonly MatrixCandidate[];
60
+ /** The dead components, each before every component it reaches, each identifier once per run. */
61
+ readonly components: readonly Component[];
62
+ }
63
+
64
+ /** Two strings ordered bytewise. */
65
+ function compare(a: string, b: string): number {
66
+ if (a === b) {
67
+ return 0;
68
+ }
69
+ return a < b ? -1 : 1;
70
+ }
71
+
72
+ /** What makes one reference one reference, whichever configuration made it. */
73
+ function referenceKey(reference: Reference): string {
74
+ const { path, line, column } = reference.position;
75
+ return [reference.from, reference.to, path, String(line), String(column), reference.use].join(
76
+ "\u0000",
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Indexes what the passes answered for each configuration of one run, in the run's order.
82
+ *
83
+ * A declaration is keyed across configurations by its identifier, which is its rendered
84
+ * position and so is the same in every configuration that compiles its file; the fields
85
+ * of the first configuration that holds it are the ones kept. A declaration one
86
+ * configuration alone holds is in the union with that one configuration, so it is
87
+ * judged where it exists rather than skipped.
88
+ *
89
+ * One reference written at one position of a file several configurations compile is seen
90
+ * once per configuration and is one reference, so the union holds it once; two references
91
+ * one configuration makes at one position are two.
92
+ */
93
+ export function matrixOf(per: readonly Configured[]): Matrix {
94
+ const symbols: InventorySymbol[] = [];
95
+ const holders = new Map<string, string[]>();
96
+ const references: Reference[] = [];
97
+ const firstSeen = new Map<string, number>();
98
+ per.forEach((one, index) => {
99
+ for (const symbol of one.symbols) {
100
+ const held = holders.get(symbol.id);
101
+ if (held === undefined) {
102
+ holders.set(symbol.id, [one.configuration]);
103
+ symbols.push(symbol);
104
+ } else if (!held.includes(one.configuration)) {
105
+ held.push(one.configuration);
106
+ }
107
+ }
108
+ for (const reference of one.references) {
109
+ const key = referenceKey(reference);
110
+ const seen = firstSeen.get(key);
111
+ if (seen === undefined) {
112
+ firstSeen.set(key, index);
113
+ } else if (seen !== index) {
114
+ continue;
115
+ }
116
+ references.push(reference);
117
+ }
118
+ });
119
+ symbols.sort((a, b) => byPosition(a.position, b.position) || compare(a.ref, b.ref));
120
+
121
+ return {
122
+ configurations: per.map((one) => one.configuration),
123
+ graphs: per.map((one) => graphOf(one.symbols, one.references, one.roots, one.testFiles)),
124
+ union: graphOf(
125
+ symbols,
126
+ references,
127
+ per.flatMap((one) => one.roots),
128
+ [...new Set(per.flatMap((one) => one.testFiles))],
129
+ ),
130
+ heldIn: symbols.map((symbol) => holders.get(symbol.id) ?? []),
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Answers which declarations of the run are dead in every configuration that holds them.
136
+ *
137
+ * Liveness is a question about one configuration, because a file another configuration
138
+ * does not compile makes no reference there and a root it does not hold seeds nothing
139
+ * there, so each configuration is swept on its own graph under the run's input. A
140
+ * declaration is a candidate when every configuration holding it judged it one: a
141
+ * reference under one configuration is a reference, so what one project uses is never
142
+ * reported, and a configuration that does not hold a declaration says nothing about it.
143
+ * The candidate's relation is reference counting where every configuration found it so,
144
+ * and reachability otherwise; its counts are the union's, one per reference.
145
+ *
146
+ * The components are computed once, over the union and the intersected dead set, so one
147
+ * dead component is one entry of the run with one identifier.
148
+ */
149
+ export function sweepMatrix(matrix: Matrix, input: SweepInput): SweepResult {
150
+ const per = matrix.graphs.map((graph) => sweep(graph, input));
151
+ const byConfiguration = new Map<string, ReadonlyMap<string, Candidate>>(
152
+ matrix.configurations.map((configuration, index) => [
153
+ configuration,
154
+ new Map((per[index]?.candidates ?? []).map((candidate) => [candidate.id, candidate])),
155
+ ]),
156
+ );
157
+
158
+ const union = matrix.union;
159
+ const dead = union.symbols.map(() => false);
160
+ const testOfDeadCode = union.symbols.map(() => false);
161
+ const candidates: MatrixCandidate[] = [];
162
+ union.symbols.forEach((symbol, at) => {
163
+ const held = matrix.heldIn[at] ?? [];
164
+ const found = held.map((configuration) => byConfiguration.get(configuration)?.get(symbol.id));
165
+ if (held.length === 0 || found.some((candidate) => candidate === undefined)) {
166
+ return;
167
+ }
168
+ const all = found.filter((candidate): candidate is Candidate => candidate !== undefined);
169
+ const admitted = all.every((candidate) => candidate.testOfDeadCode);
170
+ dead[at] = true;
171
+ testOfDeadCode[at] = admitted;
172
+ candidates.push({
173
+ id: symbol.id,
174
+ productionRefs: union.made[at]?.production ?? 0,
175
+ testRefs: union.made[at]?.test ?? 0,
176
+ relation: all.every((candidate) => candidate.relation === "reference-counting")
177
+ ? "reference-counting"
178
+ : "reachability",
179
+ testOfDeadCode: admitted,
180
+ configurations: held,
181
+ });
182
+ });
183
+
184
+ return {
185
+ liveUnder: liveAnywhere(union.symbols, per),
186
+ candidates,
187
+ components: componentsOf(union, dead, testOfDeadCode),
188
+ };
189
+ }
190
+
191
+ /** The relations in the order a declaration's entry lists them. */
192
+ const RELATIONS: readonly Relation[] = ["reference-counting", "reachability"];
193
+
194
+ /**
195
+ * Per declaration, in site order, every relation that holds it live in at least one
196
+ * configuration.
197
+ */
198
+ function liveAnywhere(
199
+ symbols: readonly InventorySymbol[],
200
+ per: readonly Liveness[],
201
+ ): ReadonlyMap<string, readonly Relation[]> {
202
+ const live = new Map<string, readonly Relation[]>();
203
+ for (const symbol of symbols) {
204
+ const relations = RELATIONS.filter((relation) =>
205
+ per.some((liveness) => (liveness.liveUnder.get(symbol.id) ?? []).includes(relation)),
206
+ );
207
+ if (relations.length > 0) {
208
+ live.set(symbol.id, relations);
209
+ }
210
+ }
211
+ return live;
212
+ }
package/src/references.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  import {
2
2
  isArrayLiteralExpression,
3
3
  isBinaryExpression,
4
+ isCallExpression,
4
5
  isDeleteExpression,
5
6
  isElementAccessExpression,
6
7
  isExportDeclaration,
8
+ isExternalModuleReference,
7
9
  isForInStatement,
8
10
  isForOfStatement,
11
+ isImportDeclaration,
12
+ isImportEqualsDeclaration,
9
13
  isNamespaceExport,
14
+ isNoSubstitutionTemplateLiteral,
10
15
  isObjectLiteralExpression,
11
16
  isParenthesizedExpression,
12
17
  isPostfixUnaryExpression,
@@ -16,11 +21,13 @@ import {
16
21
  isShorthandPropertyAssignment,
17
22
  isSpreadAssignment,
18
23
  isSpreadElement,
24
+ isStringLiteral,
19
25
  SyntaxKind,
20
26
  type Node,
21
27
  type SourceFile,
22
28
  } from "@typescript/native/unstable/ast";
23
- import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/unstable/sync";
29
+ import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
30
+ import { aliasChains } from "./alias-chain.ts";
24
31
  import { globExpression } from "./glob.ts";
25
32
  import { nodeKey, type Inventory } from "./inventory.ts";
26
33
  import { byPosition, renderPosition, type Position } from "./position.ts";
@@ -37,13 +44,18 @@ import type { ProjectView } from "./session.ts";
37
44
  *
38
45
  * What the pass records and what it does not: a reference carries its use, its
39
46
  * position, the declaration that encloses it and the classification of the file that
40
- * made it. It carries no mode. Which of the references a run counts is the reading the
41
- * sweep makes of this set, so the mode is one value the run decides once and this pass
42
- * never sees it.
47
+ * made it, and every place that imports one of the project's own modules is an
48
+ * evaluation of that module's file. It carries no mode. Which of the references a run
49
+ * counts is the reading the sweep makes of this set, so the mode is one value the run
50
+ * decides once and this pass never sees it.
43
51
  */
44
52
 
45
- /** How one reference uses the declaration it names. */
46
- export type Use = "read" | "write";
53
+ /**
54
+ * How one reference uses the declaration it names. An `evaluation` names a file: the
55
+ * place it is written imports the module, which runs the module's top level and reads
56
+ * none of what it exports. A `read` of a file reads the module's namespace.
57
+ */
58
+ export type Use = "read" | "write" | "evaluation";
47
59
 
48
60
  /**
49
61
  * Which accessor answered for one reference, so that the cost of a run is attributable
@@ -299,10 +311,52 @@ interface Link {
299
311
  readonly alias: boolean;
300
312
  }
301
313
 
314
+ /** One place the walk found that evaluates a module, and the declaration that holds it. */
315
+ interface Evaluation {
316
+ /** The module specifier, whose symbol is the module evaluated. */
317
+ readonly node: Node;
318
+ readonly from: string;
319
+ }
320
+
302
321
  /** Everything one file's walk found to resolve. */
303
322
  interface FileSites {
304
323
  readonly uses: readonly Site[];
305
324
  readonly links: readonly Link[];
325
+ readonly evaluations: readonly Evaluation[];
326
+ }
327
+
328
+ /** Whether one node is a string literal or a template literal with no substitution. */
329
+ function isLiteralText(node: Node | undefined): node is Node {
330
+ return node !== undefined && (isStringLiteral(node) || isNoSubstitutionTemplateLiteral(node));
331
+ }
332
+
333
+ /**
334
+ * The module specifier of the module one node evaluates, if it evaluates one: an import
335
+ * declaration, an import-equals declaration of an external module, an export
336
+ * declaration that names a module, and an `import()` call whose argument is literal
337
+ * text. A type-only form evaluates nothing. An `import()` of a computed argument names
338
+ * no module this pass can know.
339
+ */
340
+ function evaluatedBy(node: Node): Node | undefined {
341
+ if (isImportDeclaration(node)) {
342
+ return node.importClause?.phaseModifier === SyntaxKind.TypeKeyword
343
+ ? undefined
344
+ : node.moduleSpecifier;
345
+ }
346
+ if (isExportDeclaration(node)) {
347
+ return node.isTypeOnly ? undefined : node.moduleSpecifier;
348
+ }
349
+ if (isImportEqualsDeclaration(node)) {
350
+ const reference = node.moduleReference;
351
+ return !node.isTypeOnly && isExternalModuleReference(reference)
352
+ ? reference.expression
353
+ : undefined;
354
+ }
355
+ if (isCallExpression(node) && node.expression.kind === SyntaxKind.ImportKeyword) {
356
+ const [argument] = node.arguments;
357
+ return isLiteralText(argument) ? argument : undefined;
358
+ }
359
+ return undefined;
306
360
  }
307
361
 
308
362
  /**
@@ -363,6 +417,7 @@ function sitesOf(
363
417
  ): FileSites {
364
418
  const found: Site[] = [];
365
419
  const links: Link[] = [];
420
+ const evaluations: Evaluation[] = [];
366
421
  const writes = new Set<number>();
367
422
  const declared = new Set<number>();
368
423
  const shorthands = new Map<number, Node>();
@@ -401,6 +456,10 @@ function sitesOf(
401
456
  markNames(node);
402
457
  markStores(node, writes);
403
458
  links.push(...linksOf(file, node, declarations, enclosing));
459
+ const evaluated = evaluatedBy(node);
460
+ if (evaluated !== undefined) {
461
+ evaluations.push({ node: evaluated, from: enclosing });
462
+ }
404
463
  const held = declarations.get(nodeKey(file, node));
405
464
  const outer = enclosing;
406
465
  if (held !== undefined) {
@@ -411,7 +470,7 @@ function sitesOf(
411
470
  };
412
471
 
413
472
  file.forEachChild(visit);
414
- return { uses: found, links };
473
+ return { uses: found, links, evaluations };
415
474
  }
416
475
 
417
476
  /**
@@ -431,12 +490,6 @@ function compare(a: string, b: string): number {
431
490
  return a < b ? -1 : 1;
432
491
  }
433
492
 
434
- /** One declaration one symbol reaches, and whether a step along an alias reached it. */
435
- interface Target {
436
- readonly id: string;
437
- readonly stepped: boolean;
438
- }
439
-
440
493
  /**
441
494
  * Every reference one project's own files make to the project's own declarations, in
442
495
  * position order, and the test-file rules that classified those files.
@@ -448,7 +501,8 @@ interface Target {
448
501
  * The order is by the referencing position, then by the declaration named, the
449
502
  * declaration referencing it and the use. That is a total order because one name
450
503
  * references one declaration once from one declaration, while a chain of aliases reaches
451
- * several declarations at one position.
504
+ * several declarations at one position and a bare star re-export's specifier both reads
505
+ * and evaluates the module it names.
452
506
  */
453
507
  export function references<Brand>(
454
508
  project: ProjectView<Brand>,
@@ -462,92 +516,21 @@ export function references<Brand>(
462
516
  const matched = options.testFiles.map(() => 0);
463
517
  const found: Reference[] = [];
464
518
  const tests: string[] = [];
465
- const reached = new Map<number, readonly Target[]>();
466
- const steps = new Map<number, TSSymbol | undefined>();
519
+ const chains = aliasChains(project, held);
467
520
  let batched = 0;
468
521
  let fileBatches = 0;
469
522
  let residueFallbacks = 0;
470
523
  let shorthandLookups = 0;
471
- let aliasSteps = 0;
472
524
 
473
525
  const files = project.ownSourceFiles();
474
- const ownFiles = new Set(files.map((file) => file.fileName));
526
+ const fileIds = new Set(
527
+ held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id),
528
+ );
475
529
 
476
530
  /** The identifier of the declaration one node of this program is, where it is one. */
477
531
  const declaredAt = (node: Node): string | undefined =>
478
532
  declarations.get(nodeKey(node.getSourceFile(), node));
479
533
 
480
- /**
481
- * The declarations of the inventory one symbol is.
482
- *
483
- * A declaration handle carries the file it is in, so the question is answered before
484
- * the handle is resolved: a declaration outside the project's own files belongs to
485
- * another program and is never read, which is what keeps the pass from fetching a
486
- * library file to discover that the inventory does not hold what is in it.
487
- */
488
- const declarationsOf = (symbol: TSSymbol): string[] => {
489
- const ids: string[] = [];
490
- for (const handle of symbol.declarations) {
491
- if (!ownFiles.has(handle.path)) {
492
- continue;
493
- }
494
- const node = project.declarationAt(handle)?.node;
495
- const id = node === undefined ? undefined : declaredAt(node);
496
- if (id !== undefined && !ids.includes(id)) {
497
- ids.push(id);
498
- }
499
- }
500
- return ids;
501
- };
502
-
503
- /**
504
- * What one alias names, one link along, asked once per alias whatever the number of
505
- * chains it is a link of. Only a symbol the binder flagged an alias is asked, which is
506
- * the condition the step asserts on: a default export of an expression is written in
507
- * an alias form and names nothing declared elsewhere, so its chain ends at itself.
508
- */
509
- const stepOf = (symbol: TSSymbol): TSSymbol | undefined => {
510
- if ((symbol.flags & SymbolFlags.Alias) === 0) {
511
- return undefined;
512
- }
513
- if (steps.has(symbol.id)) {
514
- return steps.get(symbol.id);
515
- }
516
- const next = project.aliasStepOf(symbol);
517
- aliasSteps += 1;
518
- steps.set(symbol.id, next);
519
- return next;
520
- };
521
-
522
- /**
523
- * The declarations one symbol names: the declaration it is, and every link of the
524
- * alias chain it stands at the head of. A re-export something imports is used and so
525
- * is the declaration behind it, so both are named; a link this project does not
526
- * declare is stepped over rather than recorded.
527
- */
528
- const targetsOf = (symbol: TSSymbol): readonly Target[] => {
529
- const cached = reached.get(symbol.id);
530
- if (cached !== undefined) {
531
- return cached;
532
- }
533
- const targets: Target[] = [];
534
- const walked = new Set<number>();
535
- let at: TSSymbol | undefined = symbol;
536
- let stepped = false;
537
- while (at !== undefined && !walked.has(at.id)) {
538
- walked.add(at.id);
539
- for (const id of declarationsOf(at)) {
540
- if (!targets.some((target) => target.id === id)) {
541
- targets.push({ id, stepped });
542
- }
543
- }
544
- at = stepOf(at);
545
- stepped = true;
546
- }
547
- reached.set(symbol.id, targets);
548
- return targets;
549
- };
550
-
551
534
  /**
552
535
  * The declarations one re-export carries forward: for an alias, the first link along
553
536
  * its chain that the inventory declares, stepping over the links it does not, such as
@@ -555,22 +538,8 @@ export function references<Brand>(
555
538
  * Each later link is a declaration of its own that references the next, so one link
556
539
  * per re-export is what lets a chain be followed one declaration at a time.
557
540
  */
558
- const carriedBy = (link: Link, symbol: TSSymbol): readonly string[] => {
559
- if (!link.alias) {
560
- return declarationsOf(symbol);
561
- }
562
- const walked = new Set<number>([symbol.id]);
563
- let at = stepOf(symbol);
564
- while (at !== undefined && !walked.has(at.id)) {
565
- walked.add(at.id);
566
- const ids = declarationsOf(at);
567
- if (ids.length > 0) {
568
- return ids;
569
- }
570
- at = stepOf(at);
571
- }
572
- return [];
573
- };
541
+ const carriedBy = (link: Link, symbol: TSSymbol): readonly string[] =>
542
+ link.alias ? chains.nextDeclared(symbol) : chains.declarationsOf(symbol);
574
543
 
575
544
  /** The symbol one site resolves to, asking only the accessor the site's form names. */
576
545
  const symbolFor = (site: Site, answered: ReadonlyMap<Node, TSSymbol>): TSSymbol | undefined => {
@@ -598,15 +567,19 @@ export function references<Brand>(
598
567
  if (test) {
599
568
  tests.push(path);
600
569
  }
601
- const { uses, links } = sitesOf(file, declarations, declaredAt(file) ?? "");
570
+ const { uses, links, evaluations } = sitesOf(file, declarations, declaredAt(file) ?? "");
602
571
 
603
572
  // One batch per capped run of the file's name nodes, the re-exports' nodes after
604
- // the uses. A shorthand is left out of the batch: its name resolves to the property
605
- // the literal declares, so the batch's answer for it would name something this
606
- // project's inventory never holds.
573
+ // the uses and the evaluated modules' specifiers last, each node once: a bare star
574
+ // re-export's specifier is a link and an evaluation both. A shorthand is left out
575
+ // of the batch: its name resolves to the property the literal declares, so the
576
+ // batch's answer for it would name something this project's inventory never holds.
607
577
  const batching = [
608
- ...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
609
- ...links.map((link) => link.node),
578
+ ...new Set([
579
+ ...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
580
+ ...links.map((link) => link.node),
581
+ ...evaluations.map((evaluation) => evaluation.node),
582
+ ]),
610
583
  ];
611
584
  const answered = new Map<Node, TSSymbol>();
612
585
  batched += batching.length;
@@ -640,6 +613,26 @@ export function references<Brand>(
640
613
  }
641
614
  }
642
615
 
616
+ for (const evaluation of evaluations) {
617
+ const module = answered.get(evaluation.node);
618
+ if (module === undefined) {
619
+ continue;
620
+ }
621
+ const position = renderPosition(file, targetRoot, evaluation.node.getStart());
622
+ for (const id of chains.declarationsOf(module)) {
623
+ if (fileIds.has(id)) {
624
+ found.push({
625
+ from: evaluation.from,
626
+ to: id,
627
+ position,
628
+ use: "evaluation",
629
+ resolution: "batch",
630
+ test,
631
+ });
632
+ }
633
+ }
634
+ }
635
+
643
636
  for (const site of uses) {
644
637
  const direct: Resolution =
645
638
  site.shorthand !== undefined
@@ -652,7 +645,9 @@ export function references<Brand>(
652
645
  continue;
653
646
  }
654
647
  const position = renderPosition(file, targetRoot, site.node.getStart());
655
- for (const target of targetsOf(symbol)) {
648
+ // A re-export something imports is used and so is the declaration behind it, so
649
+ // the use names every link of the chain its name stands at the head of.
650
+ for (const target of chains.chainOf(symbol)) {
656
651
  found.push({
657
652
  from: site.from,
658
653
  to: target.id,
@@ -680,6 +675,6 @@ export function references<Brand>(
680
675
  .map((_pattern, index) => ({ rule: patternRule(index), matched: matched[index] ?? 0 }))
681
676
  .sort((a, b) => compare(a.rule, b.rule)),
682
677
  testFilePaths: tests.sort(compare),
683
- cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps },
678
+ cost: { batched, fileBatches, residueFallbacks, shorthandLookups, aliasSteps: chains.steps },
684
679
  };
685
680
  }
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]);