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

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 (47) hide show
  1. package/dist/src/analysis.js +39 -16
  2. package/dist/src/conformance-record.js +2 -2
  3. package/dist/src/contextual-uses.js +27 -3
  4. package/dist/src/convention-rows.js +42 -0
  5. package/dist/src/conventions.js +48 -0
  6. package/dist/src/dependencies.js +109 -38
  7. package/dist/src/findings/completion.js +6 -7
  8. package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
  9. package/dist/src/html-entries.js +29 -14
  10. package/dist/src/indexed-access.js +371 -81
  11. package/dist/src/intra-function-parts.js +57 -11
  12. package/dist/src/kinds.js +6 -6
  13. package/dist/src/references.js +60 -21
  14. package/dist/src/roots.js +96 -6
  15. package/dist/src/session.js +6 -1
  16. package/dist/src/source-files.js +1 -0
  17. package/dist/src/sweep.js +29 -17
  18. package/dist/src/symbol-keys.js +114 -0
  19. package/dist/src/test-joins.js +101 -0
  20. package/dist/src/version.js +1 -1
  21. package/dist/src/workflow-steps.js +21 -15
  22. package/dist/src/workspace-programs.js +1 -1
  23. package/jsr.json +1 -1
  24. package/package.json +1 -1
  25. package/src/analysis.ts +45 -17
  26. package/src/conformance-record.ts +2 -2
  27. package/src/contextual-uses.ts +36 -4
  28. package/src/convention-rows.ts +57 -0
  29. package/src/conventions.ts +59 -0
  30. package/src/dependencies.ts +134 -46
  31. package/src/finding.ts +2 -0
  32. package/src/findings/completion.ts +7 -7
  33. package/src/findings/dependencies-and-module-machinery.ts +9 -28
  34. package/src/html-entries.ts +33 -15
  35. package/src/indexed-access.ts +468 -102
  36. package/src/intra-function-parts.ts +84 -12
  37. package/src/kinds.ts +7 -6
  38. package/src/references.ts +86 -27
  39. package/src/roots.ts +129 -6
  40. package/src/session.ts +7 -1
  41. package/src/source-files.ts +3 -0
  42. package/src/sweep.ts +29 -17
  43. package/src/symbol-keys.ts +172 -0
  44. package/src/test-joins.ts +120 -0
  45. package/src/version.ts +1 -1
  46. package/src/workflow-steps.ts +23 -16
  47. package/src/workspace-programs.ts +1 -1
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import {
9
+ isArrayBindingPattern,
9
10
  isArrowFunction,
10
11
  isAwaitExpression,
11
12
  isBigIntLiteral,
@@ -55,10 +56,12 @@ import {
55
56
  isVoidExpression,
56
57
  NodeFlags,
57
58
  SyntaxKind,
59
+ type ArrowFunction,
58
60
  type BindingName,
59
61
  type Block,
60
62
  type CallExpression,
61
63
  type CatchClause,
64
+ type FunctionExpression,
62
65
  type Identifier,
63
66
  type Node,
64
67
  type SourceFile,
@@ -473,6 +476,7 @@ export function projectParts<Brand>(
473
476
  const calls = new Map<string, boolean>();
474
477
  const specifiers = new Set<string>();
475
478
  const sites: { readonly file: SourceFile; readonly declared: Declared }[] = [];
479
+ const contextual: ContextualParameter[] = [];
476
480
 
477
481
  for (const file of files) {
478
482
  const declarationAt = (node: Node): string | undefined => {
@@ -624,15 +628,25 @@ export function projectParts<Brand>(
624
628
  });
625
629
  const reader = readers.get(one.signature);
626
630
  if (reader !== undefined) {
627
- parts.push(...parameterParts(file, targetRoot, one.id, one.signature, reader, ids, false));
631
+ const found = parameterParts(file, targetRoot, one.id, one.signature, reader, ids, false);
632
+ parts.push(...found.parts);
633
+ contextual.push(...found.contextual);
628
634
  }
629
635
  }
630
636
  for (const one of walk.values) {
631
637
  const reader = readers.get(one.signature);
632
638
  if (reader !== undefined) {
633
- parts.push(
634
- ...parameterParts(file, targetRoot, one.holder, one.signature, reader, ids, true),
639
+ const found = parameterParts(
640
+ file,
641
+ targetRoot,
642
+ one.holder,
643
+ one.signature,
644
+ reader,
645
+ ids,
646
+ true,
635
647
  );
648
+ parts.push(...found.parts);
649
+ contextual.push(...found.contextual);
636
650
  }
637
651
  }
638
652
  walk.catches.forEach(({ name, holder }, at) => {
@@ -677,7 +691,7 @@ export function projectParts<Brand>(
677
691
  }
678
692
 
679
693
  parts.push(...resultParts(project, targetRoot, sites, references, calls));
680
- return { parts, signatures, calls, specifiers };
694
+ return { parts: withContextualRead(project, parts, contextual), signatures, calls, specifiers };
681
695
  }
682
696
 
683
697
  /**
@@ -743,9 +757,38 @@ function parameterNames(signature: SignatureNode): ParameterName[] {
743
757
  return found;
744
758
  }
745
759
 
760
+ /** An unread name of a parameter that a contextual type of its function expression keeps. */
761
+ interface ContextualParameter {
762
+ readonly part: PartFact;
763
+ readonly signature: ArrowFunction | FunctionExpression;
764
+ }
765
+
766
+ /**
767
+ * The parts with each name of `contextual` read where the compiler gives its function a
768
+ * contextual type: the inference of the call holding the function may read a parameter's
769
+ * annotation, and the code handing the argument may act on what an object pattern reads.
770
+ * A question the checker leaves unanswered keeps the name read.
771
+ */
772
+ function withContextualRead<Brand>(
773
+ project: ProjectView<Brand>,
774
+ parts: readonly PartFact[],
775
+ contextual: readonly ContextualParameter[],
776
+ ): readonly PartFact[] {
777
+ if (contextual.length === 0) {
778
+ return parts;
779
+ }
780
+ const signatures = [...new Set(contextual.map((one) => one.signature))];
781
+ const answers = project.queries.contextualTypes(signatures);
782
+ const typed = new Set(signatures.filter((_signature, at) => answers[at] !== undefined));
783
+ const read = new Set(contextual.filter((one) => typed.has(one.signature)).map((one) => one.part));
784
+ return parts.map((part) => (read.has(part) ? { ...part, used: true } : part));
785
+ }
786
+
746
787
  /**
747
- * Each name one function's parameters bind and whether the function reads it. A binding
748
- * pattern counts as read where its function reads any name it binds.
788
+ * Each name one function's parameters bind and whether the function reads it, and the
789
+ * unread names a contextual type of a function expression may keep: those of a parameter
790
+ * that carries a type annotation or is an object binding pattern. A binding pattern counts
791
+ * as read where its function reads any name it binds.
749
792
  */
750
793
  function parameterParts(
751
794
  file: SourceFile,
@@ -755,7 +798,7 @@ function parameterParts(
755
798
  reader: { readonly names: readonly ParameterName[]; readonly uses: readonly Identifier[] },
756
799
  ids: ReadonlyMap<Node, Answer<number>>,
757
800
  inValue: boolean,
758
- ): PartFact[] {
801
+ ): { readonly parts: PartFact[]; readonly contextual: ContextualParameter[] } {
759
802
  // A function that reads `arguments` reads every parameter by position.
760
803
  const readsArguments =
761
804
  !isArrowFunction(signature) && identifiersNaming(signature, new Set(["arguments"])).length > 0;
@@ -766,11 +809,14 @@ function parameterParts(
766
809
  );
767
810
  const lastRead = Math.max(-1, ...reader.names.filter((_, at) => read[at]).map((one) => one.at));
768
811
  const found: PartFact[] = [];
812
+ const contextual: ContextualParameter[] = [];
813
+ const expression =
814
+ isArrowFunction(signature) || isFunctionExpression(signature) ? signature : undefined;
769
815
  reader.names.forEach(({ name, at, pattern, exempt }, index) => {
770
816
  if (exempt) {
771
817
  return;
772
818
  }
773
- found.push({
819
+ const part: PartFact = {
774
820
  code: UNUSED_PARAMETER,
775
821
  declaration,
776
822
  kind: "parameter",
@@ -779,9 +825,19 @@ function parameterParts(
779
825
  message: `parameter ${name.text} is never read in the body`,
780
826
  used: read[index] === true,
781
827
  parameter: { beforeARead: !pattern && at < lastRead, inValue },
782
- });
828
+ };
829
+ found.push(part);
830
+ const parameter = signature.parameters[at];
831
+ if (
832
+ expression !== undefined &&
833
+ !part.used &&
834
+ parameter !== undefined &&
835
+ (parameter.type !== undefined || isObjectBindingPattern(parameter.name))
836
+ ) {
837
+ contextual.push({ part, signature: expression });
838
+ }
783
839
  });
784
- return found;
840
+ return { parts: found, contextual };
785
841
  }
786
842
 
787
843
  /** One local a block declares by name, and every identifier of the block that spells it. */
@@ -792,6 +848,22 @@ interface Local {
792
848
  readonly uses: readonly Identifier[];
793
849
  }
794
850
 
851
+ /**
852
+ * The names one declaration binds that a write of its initializer stores each on its own:
853
+ * the declared name, or each element name of an array binding pattern at any depth.
854
+ */
855
+ function arrayPatternNames(name: BindingName): readonly Identifier[] {
856
+ if (isIdentifier(name)) {
857
+ return [name];
858
+ }
859
+ if (!isArrayBindingPattern(name)) {
860
+ return [];
861
+ }
862
+ return name.elements.flatMap((element) =>
863
+ isBindingElement(element) && element.name !== undefined ? arrayPatternNames(element.name) : [],
864
+ );
865
+ }
866
+
795
867
  /** The locals one block declares with `let` or `const`, each with the names that spell it. */
796
868
  function localsOf(block: Block): readonly Local[] {
797
869
  const declared: { name: Identifier; statement: number; initialized: boolean }[] = [];
@@ -807,9 +879,9 @@ function localsOf(block: Block): readonly Local[] {
807
879
  return;
808
880
  }
809
881
  for (const declaration of list.declarations) {
810
- if (isIdentifier(declaration.name)) {
882
+ for (const name of arrayPatternNames(declaration.name)) {
811
883
  declared.push({
812
- name: declaration.name,
884
+ name,
813
885
  statement: at,
814
886
  initialized: declaration.initializer !== undefined,
815
887
  });
package/src/kinds.ts CHANGED
@@ -101,7 +101,7 @@ const ROWS: readonly KindRow[] = [
101
101
  code: "DS1101",
102
102
  name: "unnecessary-export",
103
103
  languages: ["go", "ts"],
104
- rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending.",
104
+ rule: "An exported symbol whose every reference is inside the symbol's own package or module, reported as a candidate for unexporting. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface, and so is a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, because that interface fixes them whether or not the program converts the method's type to it. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. A reference from a Go external test package, a test file whose package clause names its directory's package with the suffix _test, is a reference from outside the package, because that test builds as another package and needs the export.",
105
105
  precondition:
106
106
  "Closed world only. A Go main package, and an internal/ directory tree for importers outside its parent, are closed worlds whatever the configuration declares, because no other program can import them, so this finding is reported there always. A published package is a closed world only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the package references that function or method, because the caller holds values of the type, and unexporting it would leave an exported signature naming a type its callers cannot name.",
107
107
  defaultSeverity: "warn",
@@ -113,7 +113,7 @@ const ROWS: readonly KindRow[] = [
113
113
  code: "DS1102",
114
114
  name: "unnecessary-exposure",
115
115
  languages: ["go"],
116
- rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending.",
116
+ rule: "An exported symbol of a non-internal package whose every reference is inside the target module, reported as a candidate for relocation behind an internal boundary. The subject is a package-level declaration or a method. Three subjects are excluded. An interface method is excluded because its exportedness is the contract of the interface that declares it. A struct field is excluded because an encoder reads it by name. A method that satisfies an interface some symbol uses as a type is excluded because it cannot be unexported without its type ceasing to satisfy that interface, and so is a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, because that interface fixes them whether or not the program converts the method's type to it. The finding names the narrower visibility the references support. A declared cross-language edge counts as an out-of-package reference. Where the edge's other side is unknown to the analyzer, the finding is emitted pending. A reference from a Go external test package, a test file whose package clause names its directory's package with the suffix _test, is a reference from outside the package, because that test builds as another package and needs the export.",
117
117
  precondition:
118
118
  "Closed world only. Reported always for a main package and an internal/ directory tree, and for a published package only when the configuration declares the consumer set complete and every declared consumer loads. A library with no consumer loaded and no complete consumer set declared gets this finding on its internal/ tree and its main packages and never on its published API. A symbol the rule of DS1301 holds for is not reported under this code, whatever severity the configuration gives DS1301, because the write-only finding names the defect to fix first. A type that a parameter or a result of an exported function or method of its own package names is not reported when a reference from outside the target module references that function or method, because the caller holds values of the type, and moving it behind an internal boundary would leave an exported signature naming a type its callers cannot name.",
119
119
  defaultSeverity: "warn",
@@ -235,7 +235,7 @@ const ROWS: readonly KindRow[] = [
235
235
  languages: ["go", "ts"],
236
236
  rule: "A directly declared dependency that no import in the target needs. On the Go side, a direct require whose module provides no package any target package or test variant imports. On the TypeScript side, a manifest dependency, development dependency or peer dependency the project's import closure does not need. A deletion finding whose fix would remove the last use of a dependency names that dependency.",
237
237
  precondition:
238
- "On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and four more dependencies are needed. The first is one whose installed manifest declares a command, because a command is run by name rather than imported. The second is one that the installed manifest of a needed dependency declares as a peer and does not mark optional. The third is every required peer of a dependency so held, to a fixpoint. The fourth is one that a string names under a manifest key of an applied convention row, as grammar/analysis.md states. An installed manifest is the package's manifest in the nearest node_modules directory at or above the target, and a dependency with none is decided by the import closure alone.",
238
+ "On the Go side the rule is the semantics of `go mod tidy -diff` exactly: a requirement marked indirect is never reported, because it exists to pin a transitive version and removing it changes the build list. On the TypeScript side the import closure is the files of the run's projects, and a call of `import.meta.resolve` or of `require.resolve` in them, whatever `require` names, whose first argument is a string literal or a template literal with no substitution needs each dependency an import of that specifier needs. Three more dependencies are needed: one whose command a script entry or a workflow step runs, one that a string of a configuration file names, and one that a string names under a manifest key or a package key of an applied convention row, each as grammar/analysis.md states. A command no script entry and no workflow step runs needs nothing, and a peer the installed manifest of a needed dependency requires is needed only as any other dependency is, because the package manager installs a required peer itself. A dependency of a manifest whose directory holds source only in configurations the run lists in configurations_not_built is reported at the reachability class possible, because the source that may import it was not analyzed. An installed manifest is the package's manifest in the nearest node_modules directory at or above the target, and a dependency with none is decided by the import closure alone.",
239
239
  defaultSeverity: "deny",
240
240
  maxClass: "certain",
241
241
  fixability: "manual",
@@ -319,7 +319,7 @@ const ROWS: readonly KindRow[] = [
319
319
  languages: ["go", "ts"],
320
320
  rule: "A parameter with no reference inside its function body, on a function whose signature is free to change.",
321
321
  precondition:
322
- "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not used as a value, is not a go:linkname or cgo target, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change. In TypeScript a caller may pass a function used as a value more arguments than it declares parameters, so in such a function whose signature is free but for that use, a parameter its body never reads is reported when its body reads no parameter after it, and an unread parameter before the last read one is not. A TypeScript parameter that is a binding pattern is judged by the names it binds: each name the body never reads is reported wherever the pattern stands, because removing it from the pattern moves no argument, except a name beside a rest element of the same object pattern, whose removal changes what that element holds, and the pattern counts as a read parameter when the body reads any name it binds. A rest parameter is judged as the last parameter. A TypeScript `this` parameter declares the type of `this` and receives no argument, so it is never reported. No name exempts a parameter. A TypeScript parameter whose name starts with an underscore is judged as any other is, and a Go parameter named by the blank identifier declares no name a body could read and is never reported.",
322
+ "The signature must be free, defined as follows: the function is not a method retained by interface satisfaction, is not a method whose name and signature are those of a method of an exported interface type declared by a package outside the target that the import closure holds, is not used as a value, is not a go:linkname or cgo target, is not a function the Go test driver runs (a test, benchmark or fuzz test of a test file, or TestMain), and is not a stub whose body is empty or only panics. A function literal or function expression that initializes a variable the program only calls, never passing, storing, returning or assigning it, is called by name and is not used as a value. A parameter its body never names is dead whatever the callers, so a published declaration of a library is reported too, with the fixability the vocabulary gives the kind: the signature change is a breaking change. In TypeScript a caller may pass a function used as a value more arguments than it declares parameters, so in such a function whose signature is free but for that use, a parameter its body never reads is reported when its body reads no parameter after it, and an unread parameter before the last read one is not. A parameter of a TypeScript function expression or arrow function that the compiler gives a contextual type is not reported when it carries a type annotation, because the inference of the call that holds the function may read the annotation, or when it is an object binding pattern, because the pattern reads each property it names from the argument and the code that hands the argument may act on that read. No name such a pattern binds is reported either. Any other TypeScript parameter that is a binding pattern is judged by the names it binds: each name the body never reads is reported wherever the pattern stands, because removing it from the pattern moves no argument, except a name beside a rest element of the same object pattern, whose removal changes what that element holds, and the pattern counts as a read parameter when the body reads any name it binds. A rest parameter is judged as the last parameter. A TypeScript `this` parameter declares the type of `this` and receives no argument, so it is never reported. No name exempts a parameter. A TypeScript parameter whose name starts with an underscore is judged as any other is, and a Go parameter named by the blank identifier declares no name a body could read and is never reported.",
323
323
  defaultSeverity: "warn",
324
324
  maxClass: "certain",
325
325
  fixability: "manual",
@@ -330,8 +330,9 @@ const ROWS: readonly KindRow[] = [
330
330
  code: "DS1802",
331
331
  name: "unused-receiver",
332
332
  languages: ["go"],
333
- rule: "A named method receiver with no reference inside the method body, on a method whose signature is free to change. Go permits a method with no receiver name, so the fix deletes an identifier and changes no signature.",
334
- precondition: "The same free-signature rule as unused-parameter, applied to the receiver.",
333
+ rule: "A named method receiver with no reference inside the method body. Go permits a method with no receiver name, so the fix deletes an identifier and changes no signature.",
334
+ precondition:
335
+ "None beyond the rule. Deleting a receiver name changes no signature, so a method retained by interface satisfaction, a method used as a value and a stub are reported as any other method is.",
335
336
  defaultSeverity: "warn",
336
337
  maxClass: "certain",
337
338
  fixability: "deletable",
package/src/references.ts CHANGED
@@ -51,6 +51,7 @@ import { byPosition, isComponentFile, renderPosition, type Position } from "./po
51
51
  import { DEFAULT_BATCH_CAP, propertyTables, UNANSWERED, type Answer } from "./query.ts";
52
52
  import type { ProjectView } from "./session.ts";
53
53
  import { typeQueryAliases } from "./type-query-alias.ts";
54
+ import { computedKeyNames, keyNameOf, keyedUses, type KeyedAccess } from "./symbol-keys.ts";
54
55
  import { handedOn, valuesOf } from "./value-flow.ts";
55
56
 
56
57
  /**
@@ -80,12 +81,12 @@ import { handedOn, valuesOf } from "./value-flow.ts";
80
81
  export type Use = "read" | "write" | "evaluation" | "decorator";
81
82
 
82
83
  /**
83
- * Which path answered for one reference: `batch`, a file's batched lookup; `resolved-symbol`,
84
- * the per-node lookup for its residue; `shorthand`, the value `{ a }` reads; `alias`, a step
85
- * along an import or export chain; `syntax`, the tree alone (a decorator's target, a
86
- * component's markup); `destructured`, the property a pattern names; `contextual`, the member
87
- * a literal property writes or a value's position reads; `override`, the override a base
88
- * member's call dispatches to; `by-name`, every declaration so spelled, where the checker failed.
84
+ * Which path answered for one reference: `batch`, a file's batched lookup; `resolved-symbol`, the
85
+ * per-node lookup for its residue; `shorthand`, the value `{ a }` reads; `alias`, a step along an
86
+ * import or export chain; `syntax`, the tree alone (a decorator's target, a component's markup);
87
+ * `destructured`, the property a pattern names; `contextual`, the member a literal property writes
88
+ * or a value's position reads; `override`, the override a base call dispatches to; `keyed`, the
89
+ * member a symbol key selects; `by-name`, each declaration so spelled, where the checker failed.
89
90
  */
90
91
  export type Resolution =
91
92
  | "batch"
@@ -96,6 +97,7 @@ export type Resolution =
96
97
  | "destructured"
97
98
  | "contextual"
98
99
  | "override"
100
+ | "keyed"
99
101
  | "by-name";
100
102
 
101
103
  /** One use of one declaration by one declaration. */
@@ -297,12 +299,11 @@ function unparenthesized(node: Node): Node {
297
299
  * Records the name one store writes into.
298
300
  *
299
301
  * A property access writes the member it names. An element access writes the collection
300
- * it stores into, because a collection a module only ever stores into holds nothing
301
- * anything reads, and the key is no declaration of the target. A destructuring target
302
- * writes each name it binds, through the spreads and the nested patterns it binds them
303
- * in.
302
+ * it stores into, since a collection a module only stores into holds nothing anything
303
+ * reads, and joins `accesses` for the member a unique-symbol key selects. A destructuring
304
+ * target writes each name it binds, through its spreads and nested patterns.
304
305
  */
305
- function markWrite(target: Node, writes: Set<number>): void {
306
+ function markWrite(target: Node, writes: Set<number>, accesses: Set<Node>): void {
306
307
  const held = unparenthesized(target);
307
308
  if (isName(held)) {
308
309
  writes.add(held.pos);
@@ -315,37 +316,38 @@ function markWrite(target: Node, writes: Set<number>): void {
315
316
  return;
316
317
  }
317
318
  if (isElementAccessExpression(held)) {
319
+ accesses.add(held);
318
320
  // A store through a member's value lands in storage every holder of the value
319
321
  // reads, so it reads the member; a variable only ever stored into stays written.
320
322
  if (!isPropertyAccessExpression(unparenthesized(held.expression))) {
321
- markWrite(held.expression, writes);
323
+ markWrite(held.expression, writes, accesses);
322
324
  }
323
325
  return;
324
326
  }
325
327
  if (isArrayLiteralExpression(held)) {
326
328
  for (const element of held.elements) {
327
- markWrite(element, writes);
329
+ markWrite(element, writes, accesses);
328
330
  }
329
331
  return;
330
332
  }
331
333
  if (isObjectLiteralExpression(held)) {
332
334
  for (const property of held.properties) {
333
335
  if (isPropertyAssignment(property)) {
334
- markWrite(property.initializer, writes);
336
+ markWrite(property.initializer, writes, accesses);
335
337
  continue;
336
338
  }
337
339
  if (isShorthandPropertyAssignment(property)) {
338
- markWrite(property.name, writes);
340
+ markWrite(property.name, writes, accesses);
339
341
  continue;
340
342
  }
341
343
  if (isSpreadAssignment(property)) {
342
- markWrite(property.expression, writes);
344
+ markWrite(property.expression, writes, accesses);
343
345
  }
344
346
  }
345
347
  return;
346
348
  }
347
349
  if (isSpreadElement(held)) {
348
- markWrite(held.expression, writes);
350
+ markWrite(held.expression, writes, accesses);
349
351
  }
350
352
  }
351
353
 
@@ -356,16 +358,21 @@ function markWrite(target: Node, writes: Set<number>): void {
356
358
  * holds. A store that reads its target as well records it in `reads`: a logical
357
359
  * assignment, and a compound assignment, an increment or a decrement whose value is used.
358
360
  */
359
- function markStores(node: Node, writes: Set<number>, reads: Set<number>): void {
361
+ function markStores(
362
+ node: Node,
363
+ writes: Set<number>,
364
+ reads: Set<number>,
365
+ accesses: { readonly written: Set<Node>; readonly read: Set<Node> },
366
+ ): void {
360
367
  if (isBinaryExpression(node)) {
361
368
  const operator = node.operatorToken.kind;
362
369
  if (ASSIGNMENTS.has(operator)) {
363
- markWrite(node.left, writes);
370
+ markWrite(node.left, writes, accesses.written);
364
371
  if (
365
372
  LOGICAL_ASSIGNMENTS.has(operator) ||
366
373
  (operator !== SyntaxKind.EqualsToken && !valueDiscarded(node))
367
374
  ) {
368
- markWrite(node.left, reads);
375
+ markWrite(node.left, reads, accesses.read);
369
376
  }
370
377
  }
371
378
  return;
@@ -375,20 +382,20 @@ function markStores(node: Node, writes: Set<number>, reads: Set<number>): void {
375
382
  node.operator === SyntaxKind.PlusPlusToken ||
376
383
  node.operator === SyntaxKind.MinusMinusToken
377
384
  ) {
378
- markWrite(node.operand, writes);
385
+ markWrite(node.operand, writes, accesses.written);
379
386
  if (!valueDiscarded(node)) {
380
- markWrite(node.operand, reads);
387
+ markWrite(node.operand, reads, accesses.read);
381
388
  }
382
389
  }
383
390
  return;
384
391
  }
385
392
  if (isDeleteExpression(node)) {
386
- markWrite(node.expression, writes);
393
+ markWrite(node.expression, writes, accesses.written);
387
394
  return;
388
395
  }
389
396
  if (isForInStatement(node) || isForOfStatement(node)) {
390
397
  if (node.initializer.kind !== SyntaxKind.VariableDeclarationList) {
391
- markWrite(node.initializer, writes);
398
+ markWrite(node.initializer, writes, accesses.written);
392
399
  }
393
400
  }
394
401
  }
@@ -475,6 +482,7 @@ interface FileSites {
475
482
  readonly wholes: readonly WholeRead[];
476
483
  readonly urls: readonly ModuleURL[];
477
484
  readonly decorated: readonly Decorated[];
485
+ readonly keyed: readonly KeyedAccess[];
478
486
  }
479
487
 
480
488
  /** Whether one node is `import.meta.url`. */
@@ -667,6 +675,8 @@ function sitesOf(
667
675
  const flows: FlowAt[] = [];
668
676
  const writes = new Set<number>();
669
677
  const alsoRead = new Set<number>();
678
+ const accesses = { written: new Set<Node>(), read: new Set<Node>() };
679
+ const keyed: KeyedAccess[] = [];
670
680
  const declared = new Set<number>();
671
681
  const shorthands = new Map<number, Node>();
672
682
  let enclosing = fileId;
@@ -712,7 +722,17 @@ function sitesOf(
712
722
  return;
713
723
  }
714
724
  markNames(node);
715
- markStores(node, writes, alsoRead);
725
+ markStores(node, writes, alsoRead, accesses);
726
+ const key = isElementAccessExpression(node) ? keyNameOf(node.argumentExpression) : undefined;
727
+ if (key !== undefined && isElementAccessExpression(node)) {
728
+ const written = accesses.written.has(node);
729
+ keyed.push({
730
+ access: node,
731
+ key,
732
+ from: enclosing,
733
+ use: written ? (accesses.read.has(node) ? "both" : "write") : "read",
734
+ });
735
+ }
716
736
  if (isObjectLiteralExpression(node) && !isAssignmentPattern(node)) {
717
737
  literals.push({ literal: node, from: enclosing });
718
738
  }
@@ -747,7 +767,18 @@ function sitesOf(
747
767
  };
748
768
 
749
769
  file.forEachChild(visit);
750
- return { uses: found, literals, flows, reads, links, evaluations, wholes, urls, decorated };
770
+ return {
771
+ uses: found,
772
+ literals,
773
+ flows,
774
+ reads,
775
+ links,
776
+ evaluations,
777
+ wholes,
778
+ urls,
779
+ decorated,
780
+ keyed,
781
+ };
751
782
  }
752
783
 
753
784
  /** The declaration kinds of a property an object literal declares. */
@@ -1230,7 +1261,19 @@ export function references<Brand>(
1230
1261
  }
1231
1262
  }
1232
1263
 
1233
- for (const { file, fileId, uses, reads, links, evaluations, wholes, urls, decorated } of walked) {
1264
+ const keyNames = computedKeyNames(held.symbols);
1265
+ for (const {
1266
+ file,
1267
+ fileId,
1268
+ uses,
1269
+ reads,
1270
+ links,
1271
+ evaluations,
1272
+ wholes,
1273
+ urls,
1274
+ decorated,
1275
+ keyed,
1276
+ } of walked) {
1234
1277
  const test = testFile.get(file) === true;
1235
1278
  // A component file's markup may use any binding its blocks declare at the top level.
1236
1279
  const markup = isComponentFile(file);
@@ -1449,6 +1492,22 @@ export function references<Brand>(
1449
1492
  }
1450
1493
  });
1451
1494
 
1495
+ const resolvedKeys = keyed.flatMap((access) => {
1496
+ const symbol =
1497
+ isIdentifier(access.key) && keyNames.has(access.key.text)
1498
+ ? answered.get(access.key)
1499
+ : undefined;
1500
+ return typeof symbol === "object" ? [{ access, symbol }] : [];
1501
+ });
1502
+ for (const { access, ids } of keyedUses(project, chains, resolvedKeys, tables, cap)) {
1503
+ const position = renderPosition(file, root, access.access.argumentExpression.getStart());
1504
+ for (const id of ids) {
1505
+ for (const use of access.use === "both" ? (["read", "write"] as const) : [access.use]) {
1506
+ found.push({ from: access.from, to: id, position, use, resolution: "keyed", test });
1507
+ }
1508
+ }
1509
+ }
1510
+
1452
1511
  for (const site of uses) {
1453
1512
  const direct: Resolution =
1454
1513
  site.shorthand !== undefined