@cplieger/deadset-ts 7.0.0-dev.1 → 7.0.0-dev.3

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 (113) hide show
  1. package/dist/src/analysis.js +95 -60
  2. package/dist/src/augmentations.js +14 -12
  3. package/dist/src/component-layer.js +1 -1
  4. package/dist/src/components.js +62 -20
  5. package/dist/src/configuration-files.js +230 -30
  6. package/dist/src/conformance-record.js +1 -1
  7. package/dist/src/contextual-uses.js +104 -13
  8. package/dist/src/convention-rows.js +10 -0
  9. package/dist/src/conventions.js +35 -1
  10. package/dist/src/dependencies.js +90 -32
  11. package/dist/src/discover.js +1 -1
  12. package/dist/src/edges.js +1 -1
  13. package/dist/src/element-access.js +341 -0
  14. package/dist/src/erased-stores.js +317 -0
  15. package/dist/src/exempt-classes.js +9 -22
  16. package/dist/src/exempt.js +3 -15
  17. package/dist/src/findings/completion.js +78 -16
  18. package/dist/src/findings/dependencies-and-module-machinery.js +1 -1
  19. package/dist/src/findings/emitters.js +1 -5
  20. package/dist/src/findings/self-check.js +27 -10
  21. package/dist/src/findings-pass.js +6 -5
  22. package/dist/src/in-operands.js +30 -0
  23. package/dist/src/indexed-access.js +264 -119
  24. package/dist/src/intra-function-parts.js +25 -11
  25. package/dist/src/inventory.js +28 -12
  26. package/dist/src/keyof-comparisons.js +170 -0
  27. package/dist/src/kinds.js +17 -7
  28. package/dist/src/manifest.js +6 -16
  29. package/dist/src/matrix.js +20 -2
  30. package/dist/src/query.js +3 -0
  31. package/dist/src/references.js +14 -3
  32. package/dist/src/report.js +4 -1
  33. package/dist/src/reporters/sarif.js +4 -0
  34. package/dist/src/roots.js +0 -1
  35. package/dist/src/run.js +0 -2
  36. package/dist/src/schema.js +1 -1
  37. package/dist/src/scope.js +2 -2
  38. package/dist/src/serialization-contract.js +43 -14
  39. package/dist/src/session.js +12 -8
  40. package/dist/src/suppress-file.js +58 -6
  41. package/dist/src/suppress-inline.js +14 -5
  42. package/dist/src/suppress.js +64 -35
  43. package/dist/src/unimplemented-callees.js +312 -0
  44. package/dist/src/value-reach.js +7 -7
  45. package/dist/src/verbs/analyze.js +11 -9
  46. package/dist/src/workspace-programs.js +0 -3
  47. package/dist/src/workspace-resolution.js +0 -2
  48. package/dist/src/workspace.js +0 -3
  49. package/jsr.json +1 -1
  50. package/package.json +3 -2
  51. package/src/analysis.ts +145 -82
  52. package/src/augmentations.ts +14 -11
  53. package/src/calls.ts +3 -3
  54. package/src/component-files.ts +7 -7
  55. package/src/component-layer.ts +2 -2
  56. package/src/components.ts +104 -22
  57. package/src/config.ts +4 -4
  58. package/src/configuration-files.ts +250 -31
  59. package/src/conformance-record.ts +1 -1
  60. package/src/contextual-uses.ts +135 -17
  61. package/src/convention-rows.ts +23 -0
  62. package/src/conventions.ts +53 -1
  63. package/src/dependencies.ts +119 -38
  64. package/src/discover.ts +3 -3
  65. package/src/edges.ts +3 -3
  66. package/src/element-access.ts +446 -0
  67. package/src/entry-points.ts +1 -1
  68. package/src/erased-stores.ts +413 -0
  69. package/src/exempt-classes.ts +10 -24
  70. package/src/exempt.ts +5 -29
  71. package/src/file-facts.ts +3 -1
  72. package/src/finding.ts +3 -1
  73. package/src/findings/completion.ts +97 -21
  74. package/src/findings/dependencies-and-module-machinery.ts +1 -1
  75. package/src/findings/emitters.ts +4 -9
  76. package/src/findings/self-check.ts +31 -14
  77. package/src/findings-pass.ts +14 -7
  78. package/src/graph.ts +2 -2
  79. package/src/html-entries.ts +1 -1
  80. package/src/in-operands.ts +45 -0
  81. package/src/indexed-access.ts +303 -137
  82. package/src/intra-function-parts.ts +41 -16
  83. package/src/inventory.ts +37 -33
  84. package/src/json-document.ts +1 -1
  85. package/src/keyof-comparisons.ts +211 -0
  86. package/src/kinds.ts +20 -7
  87. package/src/manifest.ts +7 -22
  88. package/src/matrix.ts +34 -2
  89. package/src/namespace-objects.ts +1 -1
  90. package/src/pnpm-workspace.ts +1 -1
  91. package/src/query.ts +10 -0
  92. package/src/references.ts +30 -7
  93. package/src/report.ts +11 -5
  94. package/src/reporters/sarif.ts +4 -0
  95. package/src/roots.ts +0 -3
  96. package/src/run.ts +0 -2
  97. package/src/schema.ts +2 -2
  98. package/src/scope.ts +2 -2
  99. package/src/serialization-contract.ts +53 -14
  100. package/src/session.ts +34 -31
  101. package/src/stores.ts +1 -1
  102. package/src/suppress-file.ts +68 -5
  103. package/src/suppress-inline.ts +17 -7
  104. package/src/suppress.ts +79 -47
  105. package/src/template-field.ts +1 -1
  106. package/src/unimplemented-callees.ts +411 -0
  107. package/src/value-reach.ts +12 -8
  108. package/src/verbs/analyze.ts +15 -9
  109. package/src/verbs/verb.ts +1 -4
  110. package/src/workspace-programs.ts +2 -9
  111. package/src/workspace-resolution.ts +0 -3
  112. package/src/workspace-run.ts +1 -1
  113. package/src/workspace.ts +1 -9
@@ -93,8 +93,17 @@ export interface PartFact {
93
93
  readonly used: boolean;
94
94
  /** Where the part is a parameter's name, what a function used as a value decides it by. */
95
95
  readonly parameter?: ParameterPlace;
96
+ /** Where the part is a result, whether its type has a `then` member, as a promise does. */
97
+ readonly thenable?: boolean;
96
98
  }
97
99
 
100
+ /**
101
+ * What a call does with its value: uses it, discards it as a statement of its own or the
102
+ * operand of `void`, or awaits it as a statement, which sequences on a thenable value and
103
+ * discards any other.
104
+ */
105
+ type CallUse = "used" | "discarded" | "awaited";
106
+
98
107
  /** What decides a parameter of a function used as a value: a caller may pass it more arguments. */
99
108
  interface ParameterPlace {
100
109
  /** Whether a parameter after this one, which is no binding-pattern name, is read. */
@@ -116,8 +125,8 @@ interface SignatureFact {
116
125
  export interface ProjectParts {
117
126
  readonly parts: readonly PartFact[];
118
127
  readonly signatures: readonly SignatureFact[];
119
- /** Each name written as the callee of a call, by position, and whether the call's value is discarded. */
120
- readonly calls: ReadonlyMap<string, boolean>;
128
+ /** Each name written as the callee of a call, by position, and what the call does with its value. */
129
+ readonly calls: ReadonlyMap<string, CallUse>;
121
130
  /** Each name an import or an export specifier writes, by position, which uses no value. */
122
131
  readonly specifiers: ReadonlySet<string>;
123
132
  }
@@ -165,16 +174,15 @@ function calleeName(call: CallExpression): Identifier | undefined {
165
174
  return isPropertyAccessExpression(callee) && isIdentifier(callee.name) ? callee.name : undefined;
166
175
  }
167
176
 
168
- /**
169
- * Whether a call's value is discarded: the call is a statement of its own, awaited as a
170
- * statement, or the operand of `void`.
171
- */
172
- function discarded(call: CallExpression): boolean {
177
+ /** What a call does with its value. */
178
+ function callUse(call: CallExpression): CallUse {
173
179
  const parent = outerParent(call);
174
180
  if (isExpressionStatement(parent) || isVoidExpression(parent)) {
175
- return true;
181
+ return "discarded";
176
182
  }
177
- return isAwaitExpression(parent) && isExpressionStatement(outerParent(parent));
183
+ return isAwaitExpression(parent) && isExpressionStatement(outerParent(parent))
184
+ ? "awaited"
185
+ : "used";
178
186
  }
179
187
 
180
188
  /** Whether one statement leaves its statement list on every path: the rest of the list never runs. */
@@ -473,7 +481,7 @@ export function projectParts<Brand>(
473
481
  const kinds = new Map(held.symbols.map((symbol) => [symbol.id, symbol.kind]));
474
482
  const parts: PartFact[] = [];
475
483
  const signatures: SignatureFact[] = [];
476
- const calls = new Map<string, boolean>();
484
+ const calls = new Map<string, CallUse>();
477
485
  const specifiers = new Set<string>();
478
486
  const sites: { readonly file: SourceFile; readonly declared: Declared }[] = [];
479
487
  const contextual: ContextualParameter[] = [];
@@ -550,7 +558,7 @@ export function projectParts<Brand>(
550
558
  if (isCallExpression(node)) {
551
559
  const name = calleeName(node);
552
560
  if (name !== undefined) {
553
- calls.set(position(name), discarded(node));
561
+ calls.set(position(name), callUse(node));
554
562
  }
555
563
  }
556
564
  if (isImportSpecifier(node) || isExportSpecifier(node)) {
@@ -1004,10 +1012,12 @@ function resultParts<Brand>(
1004
1012
  root: string,
1005
1013
  sites: readonly { readonly file: SourceFile; readonly declared: Declared }[],
1006
1014
  references: readonly Reference[],
1007
- calls: ReadonlyMap<string, boolean>,
1015
+ calls: ReadonlyMap<string, CallUse>,
1008
1016
  ): PartFact[] {
1009
1017
  const discardedAt = new Set(
1010
- references.filter((one) => calls.get(positionKey(one.position)) === true).map((one) => one.to),
1018
+ references
1019
+ .filter((one) => (calls.get(positionKey(one.position)) ?? "used") !== "used")
1020
+ .map((one) => one.to),
1011
1021
  );
1012
1022
  const found: PartFact[] = [];
1013
1023
  for (const { file, declared } of sites) {
@@ -1030,6 +1040,8 @@ function resultParts<Brand>(
1030
1040
  continue;
1031
1041
  }
1032
1042
  const at = signature.type ?? (node as { readonly name?: Node }).name ?? node;
1043
+ const valued = project.queries.nonNullable(result);
1044
+ const members = isAnswered(valued) ? project.queries.propertiesOf(valued) : UNANSWERED;
1033
1045
  found.push({
1034
1046
  code: UNUSED_RESULT,
1035
1047
  declaration: id,
@@ -1038,6 +1050,7 @@ function resultParts<Brand>(
1038
1050
  position: spanOf(file, root, at),
1039
1051
  message: "result is discarded at every call site",
1040
1052
  used: false,
1053
+ thenable: !isAnswered(members) || members.some((member) => member.name === "then"),
1041
1054
  });
1042
1055
  }
1043
1056
  return found;
@@ -1077,7 +1090,7 @@ export function intraFunctionFacts(
1077
1090
  const known = parts.get(key);
1078
1091
  parts.set(key, known === undefined ? part : { ...known, used: known.used || part.used });
1079
1092
  }
1080
- const calls = new Map<string, boolean>();
1093
+ const calls = new Map<string, CallUse>();
1081
1094
  const specifiers = new Set<string>();
1082
1095
  for (const one of projects) {
1083
1096
  for (const [at, value] of one.calls) {
@@ -1089,6 +1102,7 @@ export function intraFunctionFacts(
1089
1102
  }
1090
1103
  const valued = new Set<string>();
1091
1104
  const called = new Map<string, boolean>();
1105
+ const awaited = new Set<string>();
1092
1106
  for (const reference of references) {
1093
1107
  if (reference.use === "decorator" || reference.use === "evaluation") {
1094
1108
  continue;
@@ -1096,7 +1110,10 @@ export function intraFunctionFacts(
1096
1110
  const at = positionKey(reference.position);
1097
1111
  const call = reference.consumer === undefined ? calls.get(at) : undefined;
1098
1112
  if (call !== undefined) {
1099
- called.set(reference.to, (called.get(reference.to) ?? true) && call);
1113
+ called.set(reference.to, (called.get(reference.to) ?? true) && call !== "used");
1114
+ if (call === "awaited") {
1115
+ awaited.add(reference.to);
1116
+ }
1100
1117
  } else if (reference.consumer !== undefined || !specifiers.has(at)) {
1101
1118
  valued.add(reference.to);
1102
1119
  }
@@ -1115,8 +1132,16 @@ export function intraFunctionFacts(
1115
1132
  .map((one) => one.declaration)
1116
1133
  .filter((id) => !fixed.has(id) && !exempted.has(id) && valued.has(id)),
1117
1134
  );
1135
+ // Awaiting a thenable result sequences on it, so a call that awaits one uses it.
1136
+ const sequenced = new Set(
1137
+ [...parts.values()]
1138
+ .filter((part) => part.thenable === true && awaited.has(part.declaration))
1139
+ .map((part) => part.declaration),
1140
+ );
1118
1141
  const discardedEverywhere = new Set(
1119
- [...called].filter(([id, all]) => all && !valued.has(id)).map(([id]) => id),
1142
+ [...called]
1143
+ .filter(([id, all]) => all && !valued.has(id) && !sequenced.has(id))
1144
+ .map(([id]) => id),
1120
1145
  );
1121
1146
  return { parts: [...parts.values()], free, freeButValued, discardedEverywhere };
1122
1147
  }
package/src/inventory.ts CHANGED
@@ -139,8 +139,6 @@ export interface InventoryCost {
139
139
 
140
140
  /** One project's declarations, in position order, and what reading them cost. */
141
141
  export interface Inventory {
142
- /** The compiler configuration the project was opened from. */
143
- readonly configFile: string;
144
142
  readonly symbols: readonly InventorySymbol[];
145
143
  /**
146
144
  * Each declaring node's {@link nodeKey} mapped to the identifier of the declaration
@@ -154,20 +152,6 @@ export interface Inventory {
154
152
  */
155
153
  readonly declarations: ReadonlyMap<string, string>;
156
154
  readonly cost: InventoryCost;
157
- /**
158
- * The module specifiers of the bare star re-exports the project's files carry, in
159
- * the order they were read. A bare star re-export declares nothing here: the names
160
- * it carries forward are the declarations of the module it names, so it is a
161
- * reference to that module and not a declaration of this one.
162
- */
163
- readonly starReExports: readonly string[];
164
- /**
165
- * The number of member declarations that were left out because the file declaring
166
- * them is not one of the target's own files. Such a declaration belongs to another
167
- * program, so the target's inventory does not name it, and the count is reported so
168
- * that the omission is a number rather than a silence.
169
- */
170
- readonly outsideOwnFiles: number;
171
155
  /**
172
156
  * The declarations whose module, namespace or container table the checker did not
173
157
  * answer, in position order: whether each is exported, and which members a table
@@ -181,6 +165,12 @@ export interface Inventory {
181
165
  readonly byName: ReadonlyMap<string, readonly string[]>;
182
166
  /** Each `declare module` declaration whose name is a string literal, by id, to that name. */
183
167
  readonly modules: ReadonlyMap<string, string>;
168
+ /**
169
+ * Each enum member's dependents, by id: the later members of its declaration with no
170
+ * initializer, up to the first one with an initializer, whose values its deletion moves.
171
+ * A member with none has no entry.
172
+ */
173
+ readonly dependents: ReadonlyMap<string, readonly string[]>;
184
174
  }
185
175
 
186
176
  /** A declaration under construction, before its reference is rendered. */
@@ -534,9 +524,7 @@ export function inventory<Brand>(
534
524
  const byNode = new Map<string, Building>();
535
525
  const exportScopes: ExportScope[] = [];
536
526
  const containers: Container[] = [];
537
- const starReExports: string[] = [];
538
527
  const modules: (readonly [Building, string])[] = [];
539
- let outsideOwnFiles = 0;
540
528
 
541
529
  const keep = (record: Building): Building => {
542
530
  building.push(record);
@@ -782,14 +770,10 @@ export function inventory<Brand>(
782
770
  }
783
771
  }
784
772
 
785
- function walkExportDeclaration(
786
- node: { readonly exportClause?: Node; readonly moduleSpecifier?: Node },
787
- owner: Building,
788
- ): void {
773
+ function walkExportDeclaration(node: { readonly exportClause?: Node }, owner: Building): void {
789
774
  const clause = node.exportClause;
775
+ // A bare star re-export declares nothing: the names it carries are the named module's.
790
776
  if (clause === undefined) {
791
- const specifier = node.moduleSpecifier;
792
- starReExports.push(specifier === undefined ? "" : literalTextOf(specifier));
793
777
  return;
794
778
  }
795
779
  if (isNamespaceExport(clause)) {
@@ -876,7 +860,7 @@ export function inventory<Brand>(
876
860
  memberTables += 1;
877
861
  const table = project.queries.membersOf(symbol);
878
862
  if (isAnswered(table)) {
879
- outsideOwnFiles += readTable(container, table, seen, false);
863
+ readTable(container, table, seen, false);
880
864
  } else {
881
865
  unansweredOwners.add(container.owner);
882
866
  }
@@ -885,7 +869,7 @@ export function inventory<Brand>(
885
869
  memberTables += 1;
886
870
  const table = project.queries.exportsOf(symbol);
887
871
  if (isAnswered(table)) {
888
- outsideOwnFiles += readTable(container, table, seen, container.isClass);
872
+ readTable(container, table, seen, container.isClass);
889
873
  } else {
890
874
  unansweredOwners.add(container.owner);
891
875
  }
@@ -905,8 +889,7 @@ export function inventory<Brand>(
905
889
  table: ReadonlyMap<string, TSSymbol>,
906
890
  seen: Map<string, Building>,
907
891
  areStatic: boolean,
908
- ): number {
909
- let outside = 0;
892
+ ): void {
910
893
  for (const [, member] of table) {
911
894
  const declarations = member.declarations
912
895
  .map((handle) => project.declarationAt(handle))
@@ -916,7 +899,6 @@ export function inventory<Brand>(
916
899
  }
917
900
  const own = declarations.filter((held) => ownFiles.has(held.node.getSourceFile().fileName));
918
901
  if (own.length === 0) {
919
- outside += declarations.length;
920
902
  continue;
921
903
  }
922
904
  // A merged symbol's table holds the members every one of its declarations writes,
@@ -925,7 +907,6 @@ export function inventory<Brand>(
925
907
  keepMember(container, held.node, seen, areStatic);
926
908
  }
927
909
  }
928
- return outside;
929
910
  }
930
911
 
931
912
  /**
@@ -1011,25 +992,48 @@ export function inventory<Brand>(
1011
992
  return undefined;
1012
993
  }
1013
994
 
995
+ const dependents = new Map<string, readonly string[]>();
996
+ for (const container of containers.filter((one) => one.memberKind === "enum-member")) {
997
+ const ids = container.members.map((member) => {
998
+ const record = byNode.get(nodeKey(member.getSourceFile(), member));
999
+ return record === undefined ? undefined : positionKey(record.position);
1000
+ });
1001
+ container.members.forEach((_member, index) => {
1002
+ const after: string[] = [];
1003
+ for (let next = index + 1; next < container.members.length; next += 1) {
1004
+ const member = container.members[next];
1005
+ if (member === undefined || !isEnumMember(member) || member.initializer !== undefined) {
1006
+ break;
1007
+ }
1008
+ const id = ids[next];
1009
+ if (id !== undefined) {
1010
+ after.push(id);
1011
+ }
1012
+ }
1013
+ const id = ids[index];
1014
+ if (id !== undefined && after.length > 0) {
1015
+ dependents.set(id, after);
1016
+ }
1017
+ });
1018
+ }
1019
+
1014
1020
  const byId = new Map(building.map((record) => [positionKey(record.position), record]));
1015
1021
  const symbols = building
1016
1022
  .map(render)
1017
1023
  .sort((a, b) => byPosition(a.position, b.position) || (a.ref < b.ref ? -1 : 1));
1018
1024
 
1019
1025
  return {
1020
- configFile: project.configFile,
1021
1026
  symbols,
1022
1027
  declarations: new Map(
1023
1028
  [...byNode].map(([key, record]) => [key, positionKey(record.position)] as const),
1024
1029
  ),
1025
1030
  cost: { batches: 1, exportTables, memberTables },
1026
- starReExports,
1027
- outsideOwnFiles,
1028
1031
  unanswered: symbols
1029
1032
  .filter((symbol) => underUnanswered(byId.get(symbol.id), unansweredOwners))
1030
1033
  .map((symbol) => symbol.id),
1031
1034
  byName: namesOf(building),
1032
1035
  modules: new Map(modules.map(([record, name]) => [positionKey(record.position), name])),
1036
+ dependents,
1033
1037
  };
1034
1038
  }
1035
1039
 
@@ -100,7 +100,7 @@ class Walk {
100
100
  }
101
101
 
102
102
  /** What a walk of one document found. */
103
- export interface DocumentWalk {
103
+ interface DocumentWalk {
104
104
  /** The offset of the brace opening each object of the document's array member, in order. */
105
105
  readonly opened: readonly number[];
106
106
  /** The first member one object writes twice, at any depth, as the offset of its name. */
@@ -0,0 +1,211 @@
1
+ /**
2
+ * The members a comparison with a key reads: where one operand of `===`, `!==`, `==` or
3
+ * `!=`, or a `switch` expression a `case` label is compared with, is declared with a type
4
+ * that holds `keyof X`, the literal the other side has names the member of `X` under that
5
+ * key, because deleting the member makes the comparison a type error.
6
+ */
7
+
8
+ import {
9
+ isBinaryExpression,
10
+ isCaseClause,
11
+ isElementAccessExpression,
12
+ isIdentifier,
13
+ isParenthesizedExpression,
14
+ isPropertyAccessExpression,
15
+ isSwitchStatement,
16
+ isTypeOperatorNode,
17
+ SyntaxKind,
18
+ type Expression,
19
+ type Node,
20
+ type SourceFile,
21
+ type TypeNode,
22
+ } from "@typescript/native/unstable/ast";
23
+ import { SymbolFlags, type Type } from "@typescript/native/unstable/sync";
24
+ import type { AliasChains } from "./alias-chain.ts";
25
+ import { nodeKey, type Inventory } from "./inventory.ts";
26
+ import { renderPosition } from "./position.ts";
27
+ import { UNANSWERED } from "./query.ts";
28
+ import type { Reference } from "./references.ts";
29
+ import type { ProjectView } from "./session.ts";
30
+
31
+ /** The operators that compare their operands for equality. */
32
+ const EQUALITIES: ReadonlySet<SyntaxKind> = new Set([
33
+ SyntaxKind.EqualsEqualsEqualsToken,
34
+ SyntaxKind.ExclamationEqualsEqualsToken,
35
+ SyntaxKind.EqualsEqualsToken,
36
+ SyntaxKind.ExclamationEqualsToken,
37
+ ]);
38
+
39
+ /** One comparison of a keyed operand with another, and the declaration that writes it. */
40
+ interface Comparison {
41
+ readonly file: SourceFile;
42
+ readonly from: string;
43
+ readonly keyed: Expression;
44
+ readonly other: Expression;
45
+ }
46
+
47
+ function unwrapped(node: Expression): Expression {
48
+ return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
49
+ }
50
+
51
+ /** The name node an operand's symbol is read at: an identifier, or a property access's name. */
52
+ function nameOf(node: Expression): Node | undefined {
53
+ const at = unwrapped(node);
54
+ if (isIdentifier(at)) {
55
+ return at;
56
+ }
57
+ return isPropertyAccessExpression(at) ? at.name : undefined;
58
+ }
59
+
60
+ /** The operand of each `keyof` one type node holds, through every type node it is built of. */
61
+ function keyofOperands(node: TypeNode): readonly TypeNode[] {
62
+ const found: TypeNode[] = [];
63
+ const visit = (at: Node): void => {
64
+ if (isTypeOperatorNode(at) && at.operator === SyntaxKind.KeyOfKeyword) {
65
+ found.push(at.type);
66
+ return;
67
+ }
68
+ at.forEachChild(visit);
69
+ };
70
+ visit(node);
71
+ return found;
72
+ }
73
+
74
+ /** The names a literal type or a union of them spells, or none. */
75
+ function literalNames(parts: readonly Type[]): readonly string[] {
76
+ const names: string[] = [];
77
+ for (const part of parts) {
78
+ if (part.isStringLiteralType()) {
79
+ names.push(part.value);
80
+ } else if (part.isNumberLiteralType()) {
81
+ names.push(String(part.value));
82
+ } else {
83
+ return [];
84
+ }
85
+ }
86
+ return names;
87
+ }
88
+
89
+ /**
90
+ * The references the key comparisons of one project's own files make to the members their
91
+ * literal side names, each made by the declaration holding the comparison.
92
+ */
93
+ export function keyofComparisonReferences<Brand>(
94
+ project: ProjectView<Brand>,
95
+ held: Inventory,
96
+ chains: AliasChains,
97
+ root: string,
98
+ testFiles: ReadonlySet<string>,
99
+ ): readonly Reference[] {
100
+ const queries = project.queries;
101
+ const comparisons: Comparison[] = [];
102
+ for (const file of project.ownSourceFiles()) {
103
+ const fileId = held.declarations.get(nodeKey(file, file));
104
+ if (fileId === undefined) {
105
+ continue;
106
+ }
107
+ const visit = (node: Node, from: string): void => {
108
+ const holder = held.declarations.get(nodeKey(file, node)) ?? from;
109
+ if (isBinaryExpression(node) && EQUALITIES.has(node.operatorToken.kind)) {
110
+ comparisons.push({ file, from: holder, keyed: node.left, other: node.right });
111
+ comparisons.push({ file, from: holder, keyed: node.right, other: node.left });
112
+ }
113
+ if (isSwitchStatement(node)) {
114
+ for (const clause of node.caseBlock.clauses) {
115
+ if (isCaseClause(clause)) {
116
+ comparisons.push({
117
+ file,
118
+ from: holder,
119
+ keyed: node.expression,
120
+ other: clause.expression,
121
+ });
122
+ }
123
+ }
124
+ }
125
+ node.forEachChild((child) => {
126
+ visit(child, holder);
127
+ });
128
+ };
129
+ file.forEachChild((child) => {
130
+ visit(child, fileId);
131
+ });
132
+ }
133
+ const named = comparisons.flatMap((one) => {
134
+ const name = nameOf(one.keyed);
135
+ return name === undefined || isElementAccessExpression(unwrapped(one.other))
136
+ ? []
137
+ : [{ ...one, name }];
138
+ });
139
+ if (named.length === 0) {
140
+ return [];
141
+ }
142
+
143
+ // The operand's own declaration states the key type, which the checker resolves to
144
+ // the literals it admits and so no longer names the type the key indexes.
145
+ const symbols = queries.symbolsAt(named.map((one) => one.name));
146
+ const keyed = named.flatMap((one, at) => {
147
+ const asked = symbols[at];
148
+ const symbol =
149
+ asked === UNANSWERED || asked === undefined || (asked.flags & SymbolFlags.Alias) === 0
150
+ ? asked
151
+ : queries.aliased(asked);
152
+ if (symbol === UNANSWERED || symbol === undefined) {
153
+ return [];
154
+ }
155
+ const operands = symbol.declarations.flatMap((handle) => {
156
+ const declared = (
157
+ project.declarationAt(handle)?.node as { readonly type?: TypeNode } | undefined
158
+ )?.type;
159
+ return declared === undefined ? [] : keyofOperands(declared);
160
+ });
161
+ return operands.length === 0 ? [] : [{ ...one, operands }];
162
+ });
163
+ if (keyed.length === 0) {
164
+ return [];
165
+ }
166
+ const nodes = keyed.flatMap((one) => [one.other, ...one.operands]);
167
+ const types = queries.typesAt(nodes);
168
+ const typeOf = new Map<Node, Type>();
169
+ nodes.forEach((node, at) => {
170
+ const type = types[at];
171
+ if (type !== UNANSWERED && type !== undefined) {
172
+ typeOf.set(node, type);
173
+ }
174
+ });
175
+ const partsOf = (type: Type): readonly Type[] => {
176
+ const parts = queries.constituents(type);
177
+ return parts === UNANSWERED ? [type] : parts;
178
+ };
179
+
180
+ const found: Reference[] = [];
181
+ for (const one of keyed) {
182
+ const literal = typeOf.get(one.other);
183
+ const names = literal === undefined ? [] : literalNames(partsOf(literal));
184
+ if (names.length === 0) {
185
+ continue;
186
+ }
187
+ const position = renderPosition(one.file, root, one.other.getStart());
188
+ for (const operand of one.operands) {
189
+ const object = typeOf.get(operand);
190
+ for (const part of object === undefined ? [] : partsOf(object)) {
191
+ const properties = queries.propertiesOf(part);
192
+ for (const property of properties === UNANSWERED ? [] : properties) {
193
+ if (!names.includes(property.name)) {
194
+ continue;
195
+ }
196
+ for (const to of chains.declarationsOf(property)) {
197
+ found.push({
198
+ from: one.from,
199
+ to,
200
+ position,
201
+ use: "read",
202
+ resolution: "keyed",
203
+ test: testFiles.has(position.path),
204
+ });
205
+ }
206
+ }
207
+ }
208
+ }
209
+ }
210
+ return found;
211
+ }