@cplieger/deadset-ts 4.0.1 → 5.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.
Files changed (138) hide show
  1. package/README.md +4 -4
  2. package/dist/bin/component-mapper.js +5 -0
  3. package/dist/bin/mapper-connection.js +101 -0
  4. package/dist/bin/node-host.js +24 -2
  5. package/dist/src/analysis.js +301 -41
  6. package/dist/src/component-files.js +612 -0
  7. package/dist/src/component-layer.js +132 -0
  8. package/dist/src/config.js +9 -2
  9. package/dist/src/configuration-files.js +290 -0
  10. package/dist/src/conformance-record.js +3 -9
  11. package/dist/src/contextual-uses.js +240 -0
  12. package/dist/src/convention-rows.js +389 -0
  13. package/dist/src/conventions.js +565 -0
  14. package/dist/src/dependencies.js +36 -3
  15. package/dist/src/destructuring.js +6 -6
  16. package/dist/src/discover.js +10 -3
  17. package/dist/src/emit-map.js +72 -0
  18. package/dist/src/entry-point-gaps.js +81 -35
  19. package/dist/src/entry-points.js +50 -32
  20. package/dist/src/exempt-classes.js +6 -1
  21. package/dist/src/exempt.js +32 -20
  22. package/dist/src/findings/completion.js +46 -32
  23. package/dist/src/findings/emitters.js +22 -1
  24. package/dist/src/findings/interfaces.js +6 -2
  25. package/dist/src/findings/reads-and-writes.js +53 -9
  26. package/dist/src/findings/unused-declarations.js +47 -3
  27. package/dist/src/generated-file.js +24 -0
  28. package/dist/src/graph.js +13 -5
  29. package/dist/src/interface-satisfaction.js +2 -68
  30. package/dist/src/inventory.js +8 -3
  31. package/dist/src/kinds.js +6 -5
  32. package/dist/src/manifest.js +37 -11
  33. package/dist/src/matrix.js +21 -13
  34. package/dist/src/partial.js +3 -1
  35. package/dist/src/pnpm-workspace.js +120 -0
  36. package/dist/src/position.js +17 -2
  37. package/dist/src/print.js +2 -0
  38. package/dist/src/query.js +15 -0
  39. package/dist/src/references.js +404 -36
  40. package/dist/src/report.js +26 -6
  41. package/dist/src/reporters/annotations.js +10 -3
  42. package/dist/src/reporters/template.js +3 -0
  43. package/dist/src/resolve.js +37 -11
  44. package/dist/src/roots.js +37 -8
  45. package/dist/src/run.js +13 -3
  46. package/dist/src/schema.js +2 -0
  47. package/dist/src/session.js +141 -34
  48. package/dist/src/setup-failure.js +23 -0
  49. package/dist/src/source-files.js +4 -61
  50. package/dist/src/stores.js +3 -0
  51. package/dist/src/sweep.js +4 -2
  52. package/dist/src/template-field.js +38 -6
  53. package/dist/src/test-support.js +59 -0
  54. package/dist/src/type-errors.js +252 -0
  55. package/dist/src/type-query-alias.js +92 -0
  56. package/dist/src/value-flow.js +74 -0
  57. package/dist/src/verbs/analyze.js +24 -4
  58. package/dist/src/verbs/explain.js +4 -1
  59. package/dist/src/verbs/print-projects.js +17 -56
  60. package/dist/src/verbs/print-retained.js +7 -2
  61. package/dist/src/verbs/print-roots.js +7 -2
  62. package/dist/src/version-range.js +255 -0
  63. package/dist/src/version.js +1 -1
  64. package/dist/src/workspace-manifests.js +68 -0
  65. package/dist/src/workspace-programs.js +342 -0
  66. package/dist/src/workspace-resolution.js +307 -0
  67. package/dist/src/workspace-run.js +75 -0
  68. package/dist/src/workspace.js +358 -0
  69. package/jsr.json +1 -1
  70. package/package.json +3 -2
  71. package/src/analysis.ts +475 -72
  72. package/src/component-files.ts +718 -0
  73. package/src/component-layer.ts +160 -0
  74. package/src/config.ts +11 -2
  75. package/src/configuration-files.ts +370 -0
  76. package/src/conformance-record.ts +3 -10
  77. package/src/contextual-uses.ts +354 -0
  78. package/src/convention-rows.ts +448 -0
  79. package/src/conventions.ts +788 -0
  80. package/src/dependencies.ts +49 -2
  81. package/src/destructuring.ts +16 -6
  82. package/src/discover.ts +10 -6
  83. package/src/emit-map.ts +93 -0
  84. package/src/entry-point-gaps.ts +92 -38
  85. package/src/entry-points.ts +79 -36
  86. package/src/exempt-classes.ts +6 -1
  87. package/src/exempt.ts +69 -20
  88. package/src/file-facts.ts +2 -0
  89. package/src/findings/completion.ts +55 -36
  90. package/src/findings/emitters.ts +32 -1
  91. package/src/findings/interfaces.ts +9 -2
  92. package/src/findings/reads-and-writes.ts +70 -9
  93. package/src/findings/unused-declarations.ts +60 -3
  94. package/src/generated-file.ts +27 -0
  95. package/src/graph.ts +15 -4
  96. package/src/host.ts +23 -0
  97. package/src/index.ts +1 -1
  98. package/src/interface-satisfaction.ts +1 -92
  99. package/src/inventory.ts +17 -3
  100. package/src/kinds.ts +7 -5
  101. package/src/manifest.ts +47 -14
  102. package/src/matrix.ts +43 -15
  103. package/src/partial.ts +6 -1
  104. package/src/pnpm-workspace.ts +135 -0
  105. package/src/position.ts +23 -2
  106. package/src/print.ts +2 -0
  107. package/src/query.ts +26 -0
  108. package/src/references.ts +472 -40
  109. package/src/report.ts +56 -6
  110. package/src/reporters/annotations.ts +12 -3
  111. package/src/reporters/template.ts +3 -0
  112. package/src/resolve.ts +65 -11
  113. package/src/roots.ts +70 -9
  114. package/src/run.ts +15 -5
  115. package/src/schema.ts +2 -0
  116. package/src/session.ts +218 -37
  117. package/src/setup-failure.ts +39 -0
  118. package/src/source-files.ts +4 -66
  119. package/src/stores.ts +9 -0
  120. package/src/sweep.ts +9 -2
  121. package/src/template-field.ts +42 -7
  122. package/src/test-support.ts +77 -0
  123. package/src/type-errors.ts +340 -0
  124. package/src/type-query-alias.ts +133 -0
  125. package/src/value-flow.ts +104 -0
  126. package/src/verbs/analyze.ts +38 -4
  127. package/src/verbs/explain.ts +4 -1
  128. package/src/verbs/print-projects.ts +17 -69
  129. package/src/verbs/print-retained.ts +7 -2
  130. package/src/verbs/print-roots.ts +7 -2
  131. package/src/verbs/verb.ts +6 -3
  132. package/src/version-range.ts +309 -0
  133. package/src/version.ts +1 -1
  134. package/src/workspace-manifests.ts +100 -0
  135. package/src/workspace-programs.ts +486 -0
  136. package/src/workspace-resolution.ts +393 -0
  137. package/src/workspace-run.ts +148 -0
  138. package/src/workspace.ts +463 -0
package/src/references.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  import {
2
2
  isArrayLiteralExpression,
3
+ isAsExpression,
3
4
  isBinaryExpression,
4
5
  isCallExpression,
5
6
  isDecorator,
6
7
  isDeleteExpression,
7
8
  isElementAccessExpression,
8
9
  isExportDeclaration,
10
+ isExpressionStatement,
9
11
  isExternalModuleReference,
10
12
  isForInStatement,
11
13
  isForOfStatement,
14
+ isForStatement,
12
15
  isImportDeclaration,
13
16
  isImportEqualsDeclaration,
14
17
  isNamespaceExport,
@@ -23,19 +26,23 @@ import {
23
26
  isSpreadAssignment,
24
27
  isSpreadElement,
25
28
  isStringLiteral,
29
+ isVoidExpression,
26
30
  SyntaxKind,
27
31
  type Node,
28
32
  type SourceFile,
29
33
  } from "@typescript/native/unstable/ast";
30
34
  import type { Symbol as TSSymbol } from "@typescript/native/unstable/sync";
31
35
  import { aliasChains, type ChainTarget } from "./alias-chain.ts";
36
+ import { contextualUses, type FlowAt, type LiteralAt } from "./contextual-uses.ts";
32
37
  import { destructuring, propertyReadsOf, type PropertyRead } from "./destructuring.ts";
33
38
  import { globExpression } from "./glob.ts";
34
- import { declarationsByName, nodeKey, type Inventory } from "./inventory.ts";
39
+ import { declarationsByName, nodeKey, type Inventory, type InventorySymbol } from "./inventory.ts";
35
40
  import { relativePath } from "./paths.ts";
36
- import { byPosition, renderPosition, type Position } from "./position.ts";
37
- import { DEFAULT_BATCH_CAP, UNANSWERED, type Answer } from "./query.ts";
41
+ import { byPosition, isComponentFile, renderPosition, type Position } from "./position.ts";
42
+ import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED, type Answer } from "./query.ts";
38
43
  import type { ProjectView } from "./session.ts";
44
+ import { typeQueryAliases } from "./type-query-alias.ts";
45
+ import { handedOn, valuesOf } from "./value-flow.ts";
39
46
 
40
47
  /**
41
48
  * Every use one project's own files make of the project's own declarations.
@@ -64,15 +71,22 @@ import type { ProjectView } from "./session.ts";
64
71
  export type Use = "read" | "write" | "evaluation" | "decorator";
65
72
 
66
73
  /**
67
- * Which path answered for one reference: `batch`, the batched lookup over a file's name
68
- * nodes; `resolved-symbol`, the per-node lookup for the residue a batch left;
69
- * `shorthand`, the lookup for the value `{ a }` reads; `alias`, a step along an import
70
- * or export chain; `syntax`, the tree alone, which says what a decorator is attached to;
71
- * `destructured`, the property a pattern names, on the destructured value's type; and
72
- * `by-name`, every declaration bearing the name a site spells, where the checker failed.
74
+ * Which path answered for one reference: `batch`, the batched lookup over a file's names;
75
+ * `resolved-symbol`, the per-node lookup for a batch's residue; `shorthand`, the value
76
+ * `{ a }` reads; `alias`, a step along an import or export chain; `syntax`, the tree alone
77
+ * (a decorator's target, a component's markup); `destructured`, the property a pattern
78
+ * names; `contextual`, the member a literal property writes or a value's position reads;
79
+ * `by-name`, every declaration spelled as the site is, where the checker failed.
73
80
  */
74
81
  export type Resolution =
75
- "batch" | "resolved-symbol" | "shorthand" | "alias" | "syntax" | "destructured" | "by-name";
82
+ | "batch"
83
+ | "resolved-symbol"
84
+ | "shorthand"
85
+ | "alias"
86
+ | "syntax"
87
+ | "destructured"
88
+ | "contextual"
89
+ | "by-name";
76
90
 
77
91
  /** One use of one declaration by one declaration. */
78
92
  export interface Reference {
@@ -98,6 +112,12 @@ export interface Reference {
98
112
  * the consumer's root.
99
113
  */
100
114
  readonly consumer?: string;
115
+ /**
116
+ * On a write, the local names of the writing file's import bindings that only the
117
+ * values its file writes into the same declaration use, which deleting the
118
+ * declaration with its writes deletes too.
119
+ */
120
+ readonly alone?: readonly string[];
101
121
  }
102
122
 
103
123
  /** The consumer one reference pass reads the files of, beside the target it names. */
@@ -144,6 +164,13 @@ export interface ReferenceCost {
144
164
  * pattern destructures, and each step a nested assignment target takes into its value.
145
165
  */
146
166
  readonly patternLookups: number;
167
+ /**
168
+ * Batched lookups for the contextual uses: the contextual types of the object literals
169
+ * and of the values that carry a member, and the values' types, each per capped run.
170
+ */
171
+ readonly contextualBatches: number;
172
+ /** Per-type lookups for the contextual uses: each distinct type's constituents, properties and symbol. */
173
+ readonly contextualLookups: number;
147
174
  }
148
175
 
149
176
  /** One project's references, in position order, and what reading them cost. */
@@ -184,9 +211,8 @@ export interface ReferenceOptions {
184
211
 
185
212
  /**
186
213
  * The operators whose left side one assignment stores into. A compound assignment is
187
- * among them: it reads its target only to write the result back, so nothing else
188
- * receives what it read, and a declaration whose every other reference is one of these
189
- * carries no information out of itself.
214
+ * among them: where its value is discarded it reads its target only to write the result
215
+ * back, so nothing else receives what it read.
190
216
  */
191
217
  const ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
192
218
  SyntaxKind.EqualsToken,
@@ -207,6 +233,40 @@ const ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
207
233
  SyntaxKind.CaretEqualsToken,
208
234
  ]);
209
235
 
236
+ /**
237
+ * The assignments that read their target to decide whether to store at all, so the read
238
+ * carries information out whatever becomes of the result.
239
+ */
240
+ const LOGICAL_ASSIGNMENTS: ReadonlySet<SyntaxKind> = new Set([
241
+ SyntaxKind.BarBarEqualsToken,
242
+ SyntaxKind.AmpersandAmpersandEqualsToken,
243
+ SyntaxKind.QuestionQuestionEqualsToken,
244
+ ]);
245
+
246
+ /**
247
+ * Whether the value of one store expression is discarded: it is a statement of its own, a
248
+ * clause of a `for` loop, the operand of `void`, or the left operand of a comma.
249
+ */
250
+ function valueDiscarded(node: Node): boolean {
251
+ let child = node;
252
+ let parent = node.parent;
253
+ while (isParenthesizedExpression(parent)) {
254
+ child = parent;
255
+ parent = parent.parent;
256
+ }
257
+ if (isExpressionStatement(parent) || isVoidExpression(parent)) {
258
+ return true;
259
+ }
260
+ if (isForStatement(parent)) {
261
+ return parent.initializer === child || parent.incrementor === child;
262
+ }
263
+ return (
264
+ isBinaryExpression(parent) &&
265
+ parent.operatorToken.kind === SyntaxKind.CommaToken &&
266
+ parent.left === child
267
+ );
268
+ }
269
+
210
270
  /** Whether one node is an identifier or a private name, the two forms a name takes. */
211
271
  function isName(node: Node): boolean {
212
272
  return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
@@ -243,7 +303,11 @@ function markWrite(target: Node, writes: Set<number>): void {
243
303
  return;
244
304
  }
245
305
  if (isElementAccessExpression(held)) {
246
- markWrite(held.expression, writes);
306
+ // A store through a member's value lands in storage every holder of the value
307
+ // reads, so it reads the member; a variable only ever stored into stays written.
308
+ if (!isPropertyAccessExpression(unparenthesized(held.expression))) {
309
+ markWrite(held.expression, writes);
310
+ }
247
311
  return;
248
312
  }
249
313
  if (isArrayLiteralExpression(held)) {
@@ -277,12 +341,20 @@ function markWrite(target: Node, writes: Set<number>): void {
277
341
  * Records every store one node performs: an assignment, a compound assignment, an
278
342
  * increment or a decrement, a `delete`, and an iteration that assigns to a name declared
279
343
  * elsewhere. An iteration that declares its own name stores into nothing the inventory
280
- * holds.
344
+ * holds. A store that reads its target as well records it in `reads`: a logical
345
+ * assignment, and a compound assignment, an increment or a decrement whose value is used.
281
346
  */
282
- function markStores(node: Node, writes: Set<number>): void {
347
+ function markStores(node: Node, writes: Set<number>, reads: Set<number>): void {
283
348
  if (isBinaryExpression(node)) {
284
- if (ASSIGNMENTS.has(node.operatorToken.kind)) {
349
+ const operator = node.operatorToken.kind;
350
+ if (ASSIGNMENTS.has(operator)) {
285
351
  markWrite(node.left, writes);
352
+ if (
353
+ LOGICAL_ASSIGNMENTS.has(operator) ||
354
+ (operator !== SyntaxKind.EqualsToken && !valueDiscarded(node))
355
+ ) {
356
+ markWrite(node.left, reads);
357
+ }
286
358
  }
287
359
  return;
288
360
  }
@@ -292,6 +364,9 @@ function markStores(node: Node, writes: Set<number>): void {
292
364
  node.operator === SyntaxKind.MinusMinusToken
293
365
  ) {
294
366
  markWrite(node.operand, writes);
367
+ if (!valueDiscarded(node)) {
368
+ markWrite(node.operand, reads);
369
+ }
295
370
  }
296
371
  return;
297
372
  }
@@ -359,6 +434,8 @@ interface Read extends PropertyRead {
359
434
  /** Everything one file's walk found to resolve, and the decorators it needs no resolution for. */
360
435
  interface FileSites {
361
436
  readonly uses: readonly Site[];
437
+ readonly literals: readonly LiteralAt[];
438
+ readonly flows: readonly FlowAt[];
362
439
  readonly reads: readonly Read[];
363
440
  readonly links: readonly Link[];
364
441
  readonly evaluations: readonly Evaluation[];
@@ -443,24 +520,53 @@ function linksOf(
443
520
  return found;
444
521
  }
445
522
 
523
+ /**
524
+ * Whether one object literal is a destructuring target, written on the left of an
525
+ * assignment or as a `for` loop's initializer, through the literals and spreads it nests in.
526
+ */
527
+ function isAssignmentPattern(node: Node): boolean {
528
+ let child = node;
529
+ let parent = node.parent;
530
+ while (
531
+ isParenthesizedExpression(parent) ||
532
+ isArrayLiteralExpression(parent) ||
533
+ isObjectLiteralExpression(parent) ||
534
+ isSpreadElement(parent) ||
535
+ isSpreadAssignment(parent) ||
536
+ (isPropertyAssignment(parent) && parent.initializer === child)
537
+ ) {
538
+ child = parent;
539
+ parent = parent.parent;
540
+ }
541
+ if (isBinaryExpression(parent)) {
542
+ return parent.operatorToken.kind === SyntaxKind.EqualsToken && parent.left === child;
543
+ }
544
+ return (isForInStatement(parent) || isForOfStatement(parent)) && parent.initializer === child;
545
+ }
546
+
446
547
  /**
447
548
  * Every name node one file holds that is a use rather than a declaration, in source
448
549
  * order, each with its enclosing declaration and its use, and every re-export the file
449
550
  * writes. One walk answers all of it, because a second would have to agree with this one
450
551
  * about which nodes are names. A declaration's own declared name is left out, which is
451
- * what makes a declaration referenced only from its own site an unreferenced one.
552
+ * what makes a declaration referenced only from its own site an unreferenced one, and so
553
+ * is every node of `unread`, a subtree that is no use.
452
554
  */
453
555
  function sitesOf(
454
556
  file: SourceFile,
455
557
  declarations: ReadonlyMap<string, string>,
456
558
  fileId: string,
559
+ unread: ReadonlySet<Node>,
457
560
  ): FileSites {
458
561
  const found: Site[] = [];
459
562
  const reads: Read[] = [];
460
563
  const links: Link[] = [];
461
564
  const evaluations: Evaluation[] = [];
462
565
  const decorated: Decorated[] = [];
566
+ const literals: LiteralAt[] = [];
567
+ const flows: FlowAt[] = [];
463
568
  const writes = new Set<number>();
569
+ const alsoRead = new Set<number>();
464
570
  const declared = new Set<number>();
465
571
  const shorthands = new Map<number, Node>();
466
572
  let enclosing = fileId;
@@ -484,19 +590,30 @@ function sitesOf(
484
590
  };
485
591
 
486
592
  const visit = (node: Node): void => {
593
+ if (unread.has(node)) {
594
+ return;
595
+ }
487
596
  if (isName(node)) {
488
597
  if (!declared.has(node.pos)) {
489
- found.push({
490
- node,
491
- shorthand: shorthands.get(node.pos),
492
- from: enclosing,
493
- use: writes.has(node.pos) ? "write" : "read",
494
- });
598
+ const site = { node, shorthand: shorthands.get(node.pos), from: enclosing };
599
+ if (writes.has(node.pos)) {
600
+ found.push({ ...site, use: "write" });
601
+ }
602
+ if (!writes.has(node.pos) || alsoRead.has(node.pos)) {
603
+ found.push({ ...site, use: "read" });
604
+ }
495
605
  }
496
606
  return;
497
607
  }
498
608
  markNames(node);
499
- markStores(node, writes);
609
+ markStores(node, writes, alsoRead);
610
+ if (isObjectLiteralExpression(node) && !isAssignmentPattern(node)) {
611
+ literals.push({ literal: node, from: enclosing });
612
+ }
613
+ // An assertion needs only comparability, so no member of its operand is read by it.
614
+ for (const written of isAsExpression(node) ? [] : handedOn(node)) {
615
+ flows.push(...valuesOf(written).map((value) => ({ value, from: enclosing })));
616
+ }
500
617
  for (const read of propertyReadsOf(node)) {
501
618
  reads.push({ ...read, from: enclosing });
502
619
  }
@@ -520,9 +637,21 @@ function sitesOf(
520
637
  };
521
638
 
522
639
  file.forEachChild(visit);
523
- return { uses: found, reads, links, evaluations, decorated };
640
+ return { uses: found, literals, flows, reads, links, evaluations, decorated };
524
641
  }
525
642
 
643
+ /** The declaration kinds of a property an object literal declares. */
644
+ const LITERAL_PROPERTY_KINDS: ReadonlySet<SyntaxKind> = new Set([
645
+ SyntaxKind.PropertyAssignment,
646
+ SyntaxKind.ShorthandPropertyAssignment,
647
+ SyntaxKind.MethodDeclaration,
648
+ SyntaxKind.GetAccessor,
649
+ SyntaxKind.SetAccessor,
650
+ ]);
651
+
652
+ /** The rule a file the analysis classified as test-support code is counted under. */
653
+ const TEST_SUPPORT_RULE = "test-support";
654
+
526
655
  /**
527
656
  * The name one test-file rule takes. A rule's name is a word list the report's own
528
657
  * vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
@@ -539,17 +668,20 @@ function patternRule(index: number): string {
539
668
  export function testFileRulesOf(
540
669
  testFiles: readonly string[],
541
670
  paths: readonly string[],
671
+ supportFiles: readonly string[] = [],
542
672
  ): readonly TestFileRule[] {
543
673
  const distinct = [...new Set(paths)];
544
- return testFiles
545
- .map((pattern, index) => {
674
+ const support = new Set(supportFiles).size;
675
+ return [
676
+ ...testFiles.map((pattern, index) => {
546
677
  const expression = globExpression(pattern);
547
678
  return {
548
679
  rule: patternRule(index),
549
680
  matched: distinct.filter((path) => expression.test(path)).length,
550
681
  };
551
- })
552
- .sort((a, b) => compare(a.rule, b.rule));
682
+ }),
683
+ ...(support === 0 ? [] : [{ rule: TEST_SUPPORT_RULE, matched: support }]),
684
+ ].sort((a, b) => compare(a.rule, b.rule));
553
685
  }
554
686
 
555
687
  const importedCache = new WeakMap<
@@ -599,6 +731,101 @@ function importedNames(file: SourceFile): ReadonlyMap<string, readonly (string |
599
731
  return names;
600
732
  }
601
733
 
734
+ /**
735
+ * The name node of every import binding one file's top level declares, and whether it
736
+ * binds a whole module.
737
+ */
738
+ function importBindings(file: SourceFile): readonly { node: Node; whole: boolean }[] {
739
+ const found: { node: Node; whole: boolean }[] = [];
740
+ for (const statement of file.statements) {
741
+ if (isImportEqualsDeclaration(statement)) {
742
+ found.push({ node: statement.name, whole: true });
743
+ continue;
744
+ }
745
+ const clause = isImportDeclaration(statement) ? statement.importClause : undefined;
746
+ if (clause === undefined) {
747
+ continue;
748
+ }
749
+ if (clause.name !== undefined) {
750
+ found.push({ node: clause.name, whole: false });
751
+ }
752
+ const bindings = clause.namedBindings;
753
+ if (bindings === undefined) {
754
+ continue;
755
+ }
756
+ if (bindings.kind === SyntaxKind.NamespaceImport) {
757
+ found.push({ node: bindings.name, whole: true });
758
+ } else {
759
+ found.push(...bindings.elements.map((element) => ({ node: element.name, whole: false })));
760
+ }
761
+ }
762
+ return found;
763
+ }
764
+
765
+ /** The value one assignment stores through the name it writes, where it is a plain assignment. */
766
+ function storedValue(name: Node): Node | undefined {
767
+ let target: Node = name;
768
+ if (isPropertyAccessExpression(name.parent) && name.parent.name === name) {
769
+ target = name.parent;
770
+ }
771
+ while (isParenthesizedExpression(target.parent)) {
772
+ target = target.parent;
773
+ }
774
+ const parent = target.parent;
775
+ return isBinaryExpression(parent) &&
776
+ parent.operatorToken.kind === SyntaxKind.EqualsToken &&
777
+ parent.left === target
778
+ ? parent.right
779
+ : undefined;
780
+ }
781
+
782
+ /**
783
+ * The import bindings of one file that only the values written into one declaration
784
+ * use, for each write into it. A binding is named by an identifier that spells its
785
+ * local name and is not the member a property access names.
786
+ */
787
+ function aloneIn(
788
+ file: SourceFile,
789
+ uses: readonly Site[],
790
+ writes: readonly { index: number; value: Node }[],
791
+ found: readonly Reference[],
792
+ ): readonly { index: number; names: readonly string[] }[] {
793
+ const bindings = new Set(importBindings(file).map((binding) => binding.node.getText()));
794
+ if (bindings.size === 0) {
795
+ return [];
796
+ }
797
+ const occurrences = new Map<string, Node[]>();
798
+ for (const site of uses) {
799
+ const text = site.node.getText();
800
+ const parent = site.node.parent;
801
+ if (!bindings.has(text) || (isPropertyAccessExpression(parent) && parent.name === site.node)) {
802
+ continue;
803
+ }
804
+ if (!occurrences.get(text)?.includes(site.node)) {
805
+ occurrences.set(text, [...(occurrences.get(text) ?? []), site.node]);
806
+ }
807
+ }
808
+ const inside = (node: Node, value: Node): boolean =>
809
+ node.pos >= value.pos && node.end <= value.end;
810
+ const result: { index: number; names: readonly string[] }[] = [];
811
+ for (const write of writes) {
812
+ const to = found[write.index]?.to;
813
+ const sameTarget = writes.filter((one) => found[one.index]?.to === to);
814
+ const names = [...occurrences]
815
+ .filter(
816
+ ([, nodes]) =>
817
+ nodes.some((node) => inside(node, write.value)) &&
818
+ nodes.every((node) => sameTarget.some((one) => inside(node, one.value))),
819
+ )
820
+ .map(([name]) => name)
821
+ .sort(compare);
822
+ if (names.length > 0) {
823
+ result.push({ index: write.index, names });
824
+ }
825
+ }
826
+ return result;
827
+ }
828
+
602
829
  /** Two strings ordered bytewise, which is the order every set of a run is read in. */
603
830
  function compare(a: string, b: string): number {
604
831
  if (a === b) {
@@ -635,7 +862,8 @@ export function references<Brand>(
635
862
  const found: Reference[] = [];
636
863
  const tests: string[] = [];
637
864
  const chains = aliasChains(project, held);
638
- const destructured = destructuring(project, cap);
865
+ const tables = propertyTables(project.queries);
866
+ const destructured = destructuring(project, cap, tables);
639
867
  let batched = 0;
640
868
  let fileBatches = 0;
641
869
  let residueFallbacks = 0;
@@ -653,9 +881,24 @@ export function references<Brand>(
653
881
  relativePath(consumer.root, file.fileName) !== undefined &&
654
882
  relativePath(targetRoot, file.fileName) === undefined,
655
883
  );
884
+ const ownPaths = project.ownPaths();
656
885
  const fileIds = new Set(
657
886
  held.symbols.filter((symbol) => symbol.kind === "file").map((symbol) => symbol.id),
658
887
  );
888
+ let topLevel: ReadonlyMap<string, readonly InventorySymbol[]> | undefined;
889
+ /** The declarations one file's top level holds. */
890
+ const declaredIn = (fileId: string): readonly InventorySymbol[] => {
891
+ if (topLevel === undefined) {
892
+ const byParent = new Map<string, InventorySymbol[]>();
893
+ for (const symbol of held.symbols) {
894
+ if (fileIds.has(symbol.parent)) {
895
+ byParent.set(symbol.parent, [...(byParent.get(symbol.parent) ?? []), symbol]);
896
+ }
897
+ }
898
+ topLevel = byParent;
899
+ }
900
+ return topLevel.get(fileId) ?? [];
901
+ };
659
902
 
660
903
  /** The identifier of the declaration one node of this program is, where it is one. */
661
904
  const declaredAt = (node: Node): string | undefined =>
@@ -693,6 +936,49 @@ export function references<Brand>(
693
936
  ];
694
937
  };
695
938
 
939
+ // A type-query alias's own type names its export without using it; a use of the
940
+ // global is the use, so it names every link of the export's chain as well.
941
+ const aliases = project.ownSourceFiles().flatMap(typeQueryAliases);
942
+ const unread = new Set(aliases.map((alias) => alias.type));
943
+ const aliased = new Map<string, readonly ChainTarget[]>();
944
+ if (aliases.length > 0) {
945
+ const exported = project.symbolsAt(aliases.map((alias) => project.handle(alias.exported)));
946
+ aliases.forEach((alias, index) => {
947
+ const id = declaredAt(alias.declaration);
948
+ const symbol = exported[index];
949
+ if (id === undefined || symbol === undefined) {
950
+ return;
951
+ }
952
+ aliased.set(
953
+ id,
954
+ symbol === UNANSWERED
955
+ ? (declarationsByName(held).get(alias.name) ?? []).map((one) => ({
956
+ id: one,
957
+ stepped: true,
958
+ guessed: true,
959
+ }))
960
+ : chains.chainOf(symbol).map((target) => ({ ...target, stepped: true })),
961
+ );
962
+ });
963
+ }
964
+ /** One use's targets, then every link an alias global among them stands for, each once. */
965
+ const throughAliases = (targets: readonly ChainTarget[]): readonly ChainTarget[] => {
966
+ if (aliased.size === 0) {
967
+ return targets;
968
+ }
969
+ const all = [...targets];
970
+ const seen = new Set(targets.map((target) => target.id));
971
+ for (const target of targets) {
972
+ for (const forwarded of aliased.get(target.id) ?? []) {
973
+ if (!seen.has(forwarded.id)) {
974
+ seen.add(forwarded.id);
975
+ all.push(forwarded);
976
+ }
977
+ }
978
+ }
979
+ return all;
980
+ };
981
+
696
982
  /** The symbol one site resolves to, asking only the accessor the site's form names. */
697
983
  const symbolFor = (
698
984
  site: Site,
@@ -710,17 +996,102 @@ export function references<Brand>(
710
996
  return project.resolvedSymbolAt(project.handle(site.node));
711
997
  };
712
998
 
713
- for (const file of files) {
999
+ const walked = files.map((file) => {
1000
+ const fileId = declaredAt(file) ?? "";
1001
+ return { file, fileId, ...sitesOf(file, declarations, fileId, unread) };
1002
+ });
1003
+ const contextual = contextualUses(
1004
+ project,
1005
+ held,
1006
+ chains,
1007
+ walked.flatMap((one) => one.literals),
1008
+ walked.flatMap((one) => one.flows),
1009
+ cap,
1010
+ tables,
1011
+ );
1012
+ const writtenBy = new Map<string, readonly string[]>();
1013
+ for (const write of contextual.writes) {
1014
+ const property = write.at.parent;
1015
+ writtenBy.set(nodeKey(property.getSourceFile(), property), write.ids);
1016
+ }
1017
+ const forwardedCache = new Map<number, readonly ChainTarget[]>();
1018
+ /**
1019
+ * The members a literal property one symbol declares writes: a use of the property
1020
+ * reaches them, because the literal's type carries the data the member describes.
1021
+ */
1022
+ const forwarded = (symbol: TSSymbol): readonly ChainTarget[] => {
1023
+ const known = forwardedCache.get(symbol.id);
1024
+ if (known !== undefined) {
1025
+ return known;
1026
+ }
1027
+ const found: ChainTarget[] = [];
1028
+ if (writtenBy.size > 0) {
1029
+ for (const handle of symbol.declarations) {
1030
+ if (!LITERAL_PROPERTY_KINDS.has(handle.kind) || !ownPaths.has(handle.path)) {
1031
+ continue;
1032
+ }
1033
+ const node = project.declarationAt(handle)?.node;
1034
+ const ids =
1035
+ node === undefined ? undefined : writtenBy.get(nodeKey(node.getSourceFile(), node));
1036
+ for (const id of ids ?? []) {
1037
+ if (!found.some((target) => target.id === id)) {
1038
+ found.push({ id, stepped: true, guessed: false });
1039
+ }
1040
+ }
1041
+ }
1042
+ }
1043
+ forwardedCache.set(symbol.id, found);
1044
+ return found;
1045
+ };
1046
+ /** One resolved symbol's declarations along its chain, and the members its literal properties write. */
1047
+ const targetsOf = (symbol: TSSymbol): readonly ChainTarget[] => {
1048
+ const through = forwarded(symbol);
1049
+ return through.length === 0 ? chains.chainOf(symbol) : [...chains.chainOf(symbol), ...through];
1050
+ };
1051
+ const testFile = new Map<SourceFile, boolean>();
1052
+ for (const { file } of walked) {
714
1053
  const path = renderPosition(file, root, 0).path;
715
1054
  const test = patterns.some((pattern) => pattern.test(path));
1055
+ testFile.set(file, test);
716
1056
  if (test) {
717
1057
  tests.push(path);
718
1058
  }
719
- const { uses, reads, links, evaluations, decorated } = sitesOf(
720
- file,
721
- declarations,
722
- declaredAt(file) ?? "",
723
- );
1059
+ }
1060
+ /** Each write reference's index in `found`, by its file, with the value it stores. */
1061
+ const valued = new Map<SourceFile, { index: number; value: Node }[]>();
1062
+ const holdsValue = (file: SourceFile, value: Node | undefined): void => {
1063
+ if (value !== undefined) {
1064
+ valued.set(file, [...(valued.get(file) ?? []), { index: found.length - 1, value }]);
1065
+ }
1066
+ };
1067
+ for (const [use, sites] of [
1068
+ ["write", contextual.writes],
1069
+ ["read", contextual.reads],
1070
+ ] as const) {
1071
+ for (const site of sites) {
1072
+ const file = site.at.getSourceFile();
1073
+ const position = renderPosition(file, root, site.at.getStart());
1074
+ for (const id of site.ids) {
1075
+ found.push({
1076
+ from: site.from,
1077
+ to: id,
1078
+ position,
1079
+ use,
1080
+ resolution: "contextual",
1081
+ test: testFile.get(file) === true,
1082
+ });
1083
+ if (use === "write") {
1084
+ holdsValue(file, site.at.parent);
1085
+ }
1086
+ }
1087
+ }
1088
+ }
1089
+
1090
+ for (const { file, fileId, uses, reads, links, evaluations, decorated } of walked) {
1091
+ const test = testFile.get(file) === true;
1092
+ // A component file's markup may use any binding its blocks declare at the top level.
1093
+ const markup = isComponentFile(file);
1094
+ const bindings = markup ? importBindings(file) : [];
724
1095
 
725
1096
  // A decorator is written inside the declaration it is attached to and receives it,
726
1097
  // so the declaration is used where the decorator is written, whatever the
@@ -746,6 +1117,7 @@ export function references<Brand>(
746
1117
  ...uses.filter((site) => site.shorthand === undefined).map((site) => site.node),
747
1118
  ...links.map((link) => link.node),
748
1119
  ...evaluations.map((evaluation) => evaluation.node),
1120
+ ...bindings.map((binding) => binding.node),
749
1121
  ]),
750
1122
  ];
751
1123
  const answered = new Map<Node, Answer<TSSymbol>>();
@@ -809,6 +1181,49 @@ export function references<Brand>(
809
1181
  }
810
1182
  }
811
1183
 
1184
+ if (markup) {
1185
+ for (const symbol of declaredIn(fileId)) {
1186
+ found.push({
1187
+ from: fileId,
1188
+ to: symbol.id,
1189
+ position: symbol.position,
1190
+ use: "read",
1191
+ resolution: "syntax",
1192
+ test,
1193
+ });
1194
+ }
1195
+ for (const binding of bindings) {
1196
+ const symbol = answered.get(binding.node);
1197
+ if (symbol === undefined) {
1198
+ continue;
1199
+ }
1200
+ const position = renderPosition(file, root, binding.node.getStart());
1201
+ const chained =
1202
+ symbol === UNANSWERED
1203
+ ? named(binding.node).map((id) => ({ id, stepped: false, guessed: true }))
1204
+ : chains.chainOf(symbol);
1205
+ // The markup may name any export of a module a binding holds whole.
1206
+ const targets = binding.whole
1207
+ ? chained.flatMap((target) => [
1208
+ target,
1209
+ ...declaredIn(target.id)
1210
+ .filter((one) => one.exported)
1211
+ .map((one) => ({ ...target, id: one.id })),
1212
+ ])
1213
+ : chained;
1214
+ for (const target of targets) {
1215
+ found.push({
1216
+ from: fileId,
1217
+ to: target.id,
1218
+ position,
1219
+ use: "read",
1220
+ resolution: target.guessed ? "by-name" : target.stepped ? "alias" : "batch",
1221
+ test,
1222
+ });
1223
+ }
1224
+ }
1225
+ }
1226
+
812
1227
  // A pattern names a property of the value it destructures, which no name node of
813
1228
  // the pattern resolves to: a shorthand's name is the local it binds.
814
1229
  destructured.resolve(reads).forEach((symbol, index) => {
@@ -824,7 +1239,7 @@ export function references<Brand>(
824
1239
  stepped: false,
825
1240
  guessed: true,
826
1241
  }))
827
- : chains.chainOf(symbol);
1242
+ : targetsOf(symbol);
828
1243
  for (const target of targets) {
829
1244
  found.push({
830
1245
  from: read.from,
@@ -851,10 +1266,11 @@ export function references<Brand>(
851
1266
  const position = renderPosition(file, root, site.node.getStart());
852
1267
  // A re-export something imports is used and so is the declaration behind it, so
853
1268
  // the use names every link of the chain its name stands at the head of.
854
- const targets =
1269
+ const targets = throughAliases(
855
1270
  symbol === UNANSWERED
856
1271
  ? named(site.node).map((id) => ({ id, stepped: false, guessed: true }))
857
- : chains.chainOf(symbol);
1272
+ : targetsOf(symbol),
1273
+ );
858
1274
  for (const target of targets) {
859
1275
  found.push({
860
1276
  from: site.from,
@@ -864,6 +1280,20 @@ export function references<Brand>(
864
1280
  resolution: target.guessed ? "by-name" : target.stepped ? "alias" : direct,
865
1281
  test,
866
1282
  });
1283
+ if (site.use === "write") {
1284
+ holdsValue(file, storedValue(site.node));
1285
+ }
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ const usesOf = new Map(walked.map((one) => [one.file, one.uses]));
1291
+ for (const [file, writes] of valued) {
1292
+ const uses = usesOf.get(file) ?? [];
1293
+ for (const { index, names } of aloneIn(file, uses, writes, found)) {
1294
+ const reference = found[index];
1295
+ if (reference !== undefined) {
1296
+ found[index] = { ...reference, alone: names };
867
1297
  }
868
1298
  }
869
1299
  }
@@ -891,6 +1321,8 @@ export function references<Brand>(
891
1321
  aliasSteps: chains.steps,
892
1322
  patternBatches: destructured.cost.patternBatches,
893
1323
  patternLookups: destructured.cost.patternLookups,
1324
+ contextualBatches: contextual.batches,
1325
+ contextualLookups: contextual.lookups,
894
1326
  },
895
1327
  };
896
1328
  }