@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
@@ -29,11 +29,13 @@ const PROJECT_OWNER = "";
29
29
  /** What a configuration's own reading needs: packages no deletion of a declaration removes. */
30
30
  export function configurationNeeds(packages) {
31
31
  return {
32
- packages: new Set(packages),
33
32
  uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
33
+ at: new Map([[ANYWHERE, new Set(packages)]]),
34
34
  unanswered: [],
35
35
  };
36
36
  }
37
+ /** The key of {@link ProjectNeeds.at} for a use no file of the target governs. */
38
+ const ANYWHERE = "";
37
39
  function isRecord(value) {
38
40
  return typeof value === "object" && value !== null && !Array.isArray(value);
39
41
  }
@@ -207,9 +209,10 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
207
209
  }
208
210
  const packages = configurationPackages(host, project.configFile, project.program.getCompilerOptions(), programPackages);
209
211
  const uses = new Map();
210
- const use = (owners, named) => {
212
+ const at = new Map();
213
+ const use = (owners, named, path) => {
211
214
  for (const one of named) {
212
- packages.add(one);
215
+ at.set(path, (at.get(path) ?? new Set()).add(one));
213
216
  for (const owner of owners) {
214
217
  let set = uses.get(owner);
215
218
  if (set === undefined) {
@@ -220,10 +223,13 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
220
223
  }
221
224
  }
222
225
  };
223
- use([PROJECT_OWNER], packages);
226
+ use([PROJECT_OWNER], packages, project.configFile);
224
227
  const unanswered = [];
225
228
  for (const file of project.ownSourceFiles()) {
226
- if (fileNeeds(project, held, file, programPackages, use)) {
229
+ const inFile = (owners, named) => {
230
+ use(owners, named, file.fileName);
231
+ };
232
+ if (fileNeeds(project, held, file, programPackages, inFile)) {
227
233
  unanswered.push(file.fileName);
228
234
  }
229
235
  }
@@ -241,22 +247,26 @@ export function projectNeeds(project, held, host, targetRoot, parse) {
241
247
  for (const { file } of configuration.modules) {
242
248
  const names = declaredAt(file.fileName);
243
249
  for (const { node, text } of moduleStrings(file)) {
244
- use(ownersOf(file, held, node), dependenciesNamed(text, names));
250
+ use(ownersOf(file, held, node), dependenciesNamed(text, names), file.fileName);
245
251
  }
246
252
  }
247
- for (const document of configuration.documents) {
248
- const names = declaredAt(document.path);
249
- for (const text of document.strings) {
250
- use([PROJECT_OWNER], dependenciesNamed(text, names));
253
+ for (const { strings, manifestDirs } of configuration.documents) {
254
+ for (const manifestDir of manifestDirs) {
255
+ const names = declaredAt(joinPath(manifestDir, "package.json"));
256
+ for (const text of strings) {
257
+ use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
258
+ }
251
259
  }
252
260
  }
253
- for (const { file, manifestDir } of configuration.outside) {
254
- const names = declaredAt(joinPath(manifestDir, "package.json"));
255
- for (const { text } of moduleStrings(file)) {
256
- use([PROJECT_OWNER], dependenciesNamed(text, names));
261
+ for (const { file, manifestDirs } of configuration.outside) {
262
+ for (const manifestDir of manifestDirs) {
263
+ const names = declaredAt(joinPath(manifestDir, "package.json"));
264
+ for (const { text } of moduleStrings(file)) {
265
+ use([PROJECT_OWNER], dependenciesNamed(text, names), joinPath(manifestDir, MANIFEST));
266
+ }
257
267
  }
258
268
  }
259
- return { packages, uses, unanswered };
269
+ return { uses, at, unanswered };
260
270
  }
261
271
  /** The declarations of the inventory a use written at one node belongs to. */
262
272
  function ownersOf(file, held, node) {
@@ -492,8 +502,10 @@ function installedOf(host, root, name) {
492
502
  const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
493
503
  if (read !== undefined && isRecord(read.value)) {
494
504
  const bin = read.value["bin"];
505
+ const peers = read.value["peerDependencies"];
495
506
  // A string `bin` names one command, the package's name without its scope.
496
507
  return {
508
+ peers: isRecord(peers) ? Object.keys(peers) : [],
497
509
  commands: typeof bin === "string"
498
510
  ? bin === ""
499
511
  ? []
@@ -554,20 +566,49 @@ function scriptTokensOf(manifest) {
554
566
  function isInside(path, dir) {
555
567
  return dir === "" || path.startsWith(`${dir}/`);
556
568
  }
569
+ /** The directory below the target root a declared dependency's manifest is in, `""` being the root. */
570
+ function manifestDir(dependency) {
571
+ const dir = dirnamePath(dependency.position.path);
572
+ return dir === "." ? "" : dir;
573
+ }
574
+ /**
575
+ * Adds to `needed` each declared dependency that the installed manifest of a needed or
576
+ * run declared dependency names as a peer, until no further one joins: the declaration of
577
+ * that name in the needing dependency's manifest or in a manifest above it, where the
578
+ * installed peer is resolved from. `peers` holds each declaration's installed peers by
579
+ * {@link declarationKey}.
580
+ */
581
+ function addNeededPeers(declared, peers, needed, ran) {
582
+ const pending = declared.filter((dependency) => needed.has(declarationKey(dependency)) || ran.has(declarationKey(dependency)));
583
+ for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
584
+ const below = manifestDir(next);
585
+ for (const peer of peers.get(declarationKey(next)) ?? []) {
586
+ for (const dependency of declared) {
587
+ const key = declarationKey(dependency);
588
+ if (dependency.name === peer &&
589
+ !needed.has(key) &&
590
+ (below === manifestDir(dependency) || isInside(below, manifestDir(dependency)))) {
591
+ needed.add(key);
592
+ pending.push(dependency);
593
+ }
594
+ }
595
+ }
596
+ }
597
+ }
557
598
  /**
558
- * The run's dependency evidence: the target's manifest and each workspace member's in
559
- * `members` below the target root, read with the position of every dependency each
560
- * declares, what the projects and the commands its scripts and workflow steps run need,
561
- * and the last uses each deletion candidate holds. A member in `unbuilt` holds source only
562
- * in configurations the run could not build. A manifest the program cannot read declares
563
- * nothing.
599
+ * The run's dependency evidence over the target's manifest and those of `members` and
600
+ * `nested` packages below the root: each declared dependency's position, what the projects,
601
+ * scripts and workflow steps need, and each deletion candidate's last uses. A member in
602
+ * `unbuilt` holds source only in configurations the run could not build. An unreadable
603
+ * manifest declares nothing, nor does a nested package's uninstalled dependency, because
604
+ * no run requires a nested package to be installed.
564
605
  */
565
- export function dependenciesOf(host, targetRoot, perProject, candidates, members = [], unbuiltMembers = []) {
606
+ export function dependenciesOf(host, targetRoot, perProject, candidates, members = [], unbuiltMembers = [], nested = []) {
566
607
  const root = resolvePath(host.workingDirectory(), targetRoot);
567
608
  const scopeOf = packageScope(host, targetRoot);
568
609
  const manifests = new Map([[root, MANIFEST]]);
569
610
  const unbuilt = new Set();
570
- for (const dir of [...members, ...unbuiltMembers]) {
611
+ for (const dir of [...members, ...unbuiltMembers, ...nested]) {
571
612
  const below = relativePath(root, dir);
572
613
  if (below !== undefined && !manifests.has(dir)) {
573
614
  manifests.set(dir, `${below}/${MANIFEST}`);
@@ -582,6 +623,8 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
582
623
  const workflow = new Set(workflowTokens(host, targetRoot));
583
624
  const declared = [];
584
625
  const ran = new Set();
626
+ const peers = new Map();
627
+ const uninstalledHere = new Set(nested);
585
628
  for (const [dir, path] of manifests) {
586
629
  const read = readJSON(host, joinPath(dir, MANIFEST));
587
630
  if (read === undefined || !isRecord(read.value)) {
@@ -589,12 +632,16 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
589
632
  }
590
633
  const tokens = scriptTokensOf(read.value);
591
634
  for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
592
- declared.push(dependency);
593
635
  const found = installedOf(host, dir, dependency.name);
636
+ if (found === undefined && uninstalledHere.has(dir)) {
637
+ continue;
638
+ }
639
+ declared.push(dependency);
594
640
  if (found === undefined) {
595
641
  continue;
596
642
  }
597
- const runs = (command) => tokens.has(command) || (dir === root && workflow.has(command));
643
+ peers.set(declarationKey(dependency), found.peers);
644
+ const runs = (command) => tokens.has(command) || workflow.has(command);
598
645
  if (found.commands.some(runs)) {
599
646
  ran.add(declarationKey(dependency));
600
647
  }
@@ -603,25 +650,36 @@ export function dependenciesOf(host, targetRoot, perProject, candidates, members
603
650
  // A declaration several projects compile is one user, and a use any project makes is
604
651
  // a use, so the uses are merged by declaration before anything is counted.
605
652
  const needed = new Set();
653
+ const byName = new Map();
654
+ for (const dependency of declared) {
655
+ byName.set(dependency.name, [...(byName.get(dependency.name) ?? []), dependency]);
656
+ }
606
657
  const uses = new Map();
607
658
  for (const project of perProject) {
608
659
  // A specifier the checker left unanswered may resolve to any dependency a manifest at
609
660
  // or above its file declares, and what the configuration needs is never deleted.
610
661
  const unanswered = project.unanswered.flatMap((file) => relativePath(root, file) ?? []);
611
- const unknown = declared
612
- .filter((dependency) => {
613
- const dir = dirnamePath(dependency.position.path);
614
- return unanswered.some((file) => isInside(file, dir === "." ? "" : dir));
615
- })
616
- .map((dependency) => dependency.name);
662
+ const unknownDeclared = declared.filter((dependency) => unanswered.some((file) => isInside(file, manifestDir(dependency))));
663
+ unknownDeclared.forEach((dependency) => needed.add(declarationKey(dependency)));
664
+ const unknown = unknownDeclared.map((dependency) => dependency.name);
617
665
  const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)]];
618
666
  for (const [id, used] of [...project.uses, ...held]) {
619
667
  const merged = uses.get(id) ?? new Set();
620
668
  used.forEach((one) => merged.add(one));
621
669
  uses.set(id, merged);
622
670
  }
623
- [...project.packages, ...unknown].forEach((one) => needed.add(one));
671
+ for (const [path, named] of project.at) {
672
+ const below = path === ANYWHERE ? undefined : relativePath(root, path);
673
+ for (const one of named) {
674
+ for (const dependency of byName.get(one) ?? []) {
675
+ if (below === undefined || isInside(below, manifestDir(dependency))) {
676
+ needed.add(declarationKey(dependency));
677
+ }
678
+ }
679
+ }
680
+ }
624
681
  }
682
+ addNeededPeers(declared, peers, needed, ran);
625
683
  const users = new Map();
626
684
  for (const used of uses.values()) {
627
685
  used.forEach((one) => users.set(one, (users.get(one) ?? 0) + 1));
@@ -23,7 +23,7 @@ export class DiscoveryError extends Error {
23
23
  /** The category an error diagnostic carries, which the protocol sends as a number. */
24
24
  const ERROR_CATEGORY = DiagnosticCategory.Error;
25
25
  /** Whether one diagnostic is an error, which is what fails a run closed. */
26
- export function isError(diagnostic) {
26
+ function isError(diagnostic) {
27
27
  return diagnostic.category === ERROR_CATEGORY;
28
28
  }
29
29
  /**
package/dist/src/edges.js CHANGED
@@ -25,7 +25,7 @@ const EDGE_WANT = "an edge naming id, provides and used_by, each a string, and a
25
25
  const ID_WANT = "one or more solidus-separated segments of ASCII letters, digits, underscore, full stop and hyphen";
26
26
  const REFERENCE_WANT = "a stable symbol reference naming one symbol exactly, <language>://<scope>#<fragment>";
27
27
  /** An edges document the Contract refuses: a malformed input, answered with the usage code. */
28
- export class EdgesError extends ConfigError {
28
+ class EdgesError extends ConfigError {
29
29
  constructor(at, text, want) {
30
30
  super("malformed", EDGES_FILE, `${at}: ${text}: want ${want}`);
31
31
  this.name = "EdgesError";
@@ -0,0 +1,341 @@
1
+ /**
2
+ * The members an element access reads by the literal type of its index: `o[k]` with `k`
3
+ * typed `"a" | "b"` reads `a` and `b` of the type of `o`, an index typed `keyof T` reads
4
+ * every member of `T`, and an index typed as a type parameter reads what its constraint does. Where the index is a parameter of a function every call of which the
5
+ * target holds, the members are read at each call, by the type of the argument it passes.
6
+ */
7
+ import { isArrayLiteralExpression, isArrowFunction, isBinaryExpression, isCallExpression, isElementAccessExpression, isForInStatement, isForOfStatement, isFunctionDeclaration, isFunctionExpression, isIdentifier, isNoSubstitutionTemplateLiteral, isNumericLiteral, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, isStringLiteral, isVariableDeclaration, isVariableDeclarationList, isVariableStatement, NodeFlags, SyntaxKind, } from "@typescript/native/unstable/ast";
8
+ import { nodeKey } from "./inventory.js";
9
+ import { renderPosition } from "./position.js";
10
+ import { UNANSWERED } from "./query.js";
11
+ /** The assignment operators that read their target before they write it. */
12
+ const COMPOUND = new Set([
13
+ SyntaxKind.PlusEqualsToken,
14
+ SyntaxKind.MinusEqualsToken,
15
+ SyntaxKind.AsteriskEqualsToken,
16
+ SyntaxKind.AsteriskAsteriskEqualsToken,
17
+ SyntaxKind.SlashEqualsToken,
18
+ SyntaxKind.PercentEqualsToken,
19
+ SyntaxKind.LessThanLessThanEqualsToken,
20
+ SyntaxKind.GreaterThanGreaterThanEqualsToken,
21
+ SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken,
22
+ SyntaxKind.AmpersandEqualsToken,
23
+ SyntaxKind.BarEqualsToken,
24
+ SyntaxKind.CaretEqualsToken,
25
+ SyntaxKind.AmpersandAmpersandEqualsToken,
26
+ SyntaxKind.BarBarEqualsToken,
27
+ SyntaxKind.QuestionQuestionEqualsToken,
28
+ ]);
29
+ /** How an expression is used where it is written: read, written, or both. */
30
+ function useOf(node) {
31
+ let at = node;
32
+ while (isParenthesizedExpression(at.parent)) {
33
+ at = at.parent;
34
+ }
35
+ const parent = at.parent;
36
+ if (isBinaryExpression(parent) && parent.left === at) {
37
+ if (parent.operatorToken.kind === SyntaxKind.EqualsToken) {
38
+ return "write";
39
+ }
40
+ if (COMPOUND.has(parent.operatorToken.kind)) {
41
+ return "both";
42
+ }
43
+ }
44
+ if ((isPrefixUnaryExpression(parent) || isPostfixUnaryExpression(parent)) &&
45
+ (parent.operator === SyntaxKind.PlusPlusToken || parent.operator === SyntaxKind.MinusMinusToken)) {
46
+ return "both";
47
+ }
48
+ return "read";
49
+ }
50
+ function unwrapped(node) {
51
+ return isParenthesizedExpression(node) ? unwrapped(node.expression) : node;
52
+ }
53
+ /** Whether an index is a literal the checker resolves to a member by its own text. */
54
+ function isLiteralIndex(node) {
55
+ const at = unwrapped(node);
56
+ return isStringLiteral(at) || isNumericLiteral(at) || isNoSubstitutionTemplateLiteral(at);
57
+ }
58
+ /**
59
+ * The function `node` is, where every call of it is written in its own file under its own
60
+ * name: a function declaration or a `const` initialized with an arrow function or a function
61
+ * expression, neither exported. A method may be dispatched to through a base class or an
62
+ * interface, so it is none.
63
+ */
64
+ function callableOf(node) {
65
+ const exported = (holder) => (holder.modifiers ?? []).some((modifier) => modifier.kind === SyntaxKind.ExportKeyword || modifier.kind === SyntaxKind.DefaultKeyword);
66
+ if (isFunctionDeclaration(node)) {
67
+ return node.name === undefined || exported(node)
68
+ ? undefined
69
+ : { name: node.name, parameters: node.parameters, body: node.body };
70
+ }
71
+ if (!isArrowFunction(node) && !isFunctionExpression(node)) {
72
+ return undefined;
73
+ }
74
+ const declaration = node.parent;
75
+ const list = declaration.parent;
76
+ if (!isVariableDeclaration(declaration) ||
77
+ declaration.initializer !== node ||
78
+ !isIdentifier(declaration.name) ||
79
+ !isVariableDeclarationList(list) ||
80
+ (list.flags & NodeFlags.Const) === 0 ||
81
+ !isVariableStatement(list.parent) ||
82
+ exported(list.parent)) {
83
+ return undefined;
84
+ }
85
+ return { name: declaration.name, parameters: node.parameters, body: node.body };
86
+ }
87
+ /** The innermost function enclosing a node, or none. */
88
+ function enclosingFunction(node) {
89
+ for (let at = node.parent; at !== undefined; at = at.parent) {
90
+ if (isFunctionDeclaration(at) ||
91
+ isArrowFunction(at) ||
92
+ isFunctionExpression(at) ||
93
+ at.kind === SyntaxKind.MethodDeclaration ||
94
+ at.kind === SyntaxKind.Constructor ||
95
+ at.kind === SyntaxKind.GetAccessor ||
96
+ at.kind === SyntaxKind.SetAccessor) {
97
+ return at;
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+ /** Whether an assignment target, a destructuring pattern at any depth included, assigns a name. */
103
+ function targetAssigns(target, name) {
104
+ if (isParenthesizedExpression(target)) {
105
+ return targetAssigns(target.expression, name);
106
+ }
107
+ if (isIdentifier(target)) {
108
+ return target.text === name;
109
+ }
110
+ if (isArrayLiteralExpression(target)) {
111
+ return target.elements.some((element) => targetAssigns(element, name));
112
+ }
113
+ if (isObjectLiteralExpression(target)) {
114
+ return target.properties.some((property) => isPropertyAssignment(property)
115
+ ? targetAssigns(property.initializer, name)
116
+ : isShorthandPropertyAssignment(property)
117
+ ? isIdentifier(property.name) && property.name.text === name
118
+ : isSpreadAssignment(property) && targetAssigns(property.expression, name));
119
+ }
120
+ if (isSpreadElement(target)) {
121
+ return targetAssigns(target.expression, name);
122
+ }
123
+ // A pattern element with a default value, `[k = "a"] = …`, assigns its left side.
124
+ return (isBinaryExpression(target) &&
125
+ target.operatorToken.kind === SyntaxKind.EqualsToken &&
126
+ targetAssigns(target.left, name));
127
+ }
128
+ /**
129
+ * Whether a body assigns a name: by an assignment, a destructuring one included, an
130
+ * increment or a decrement, or as the target of a `for…in` or `for…of` loop.
131
+ */
132
+ function assigns(body, name) {
133
+ let found = false;
134
+ const visit = (node) => {
135
+ if (found) {
136
+ return;
137
+ }
138
+ const target = isBinaryExpression(node) &&
139
+ (node.operatorToken.kind === SyntaxKind.EqualsToken || COMPOUND.has(node.operatorToken.kind))
140
+ ? node.left
141
+ : (isPrefixUnaryExpression(node) || isPostfixUnaryExpression(node)) &&
142
+ (node.operator === SyntaxKind.PlusPlusToken ||
143
+ node.operator === SyntaxKind.MinusMinusToken)
144
+ ? node.operand
145
+ : (isForInStatement(node) || isForOfStatement(node)) &&
146
+ node.initializer.kind !== SyntaxKind.VariableDeclarationList
147
+ ? node.initializer
148
+ : undefined;
149
+ found = target !== undefined && targetAssigns(target, name);
150
+ node.forEachChild(visit);
151
+ };
152
+ if (body !== undefined) {
153
+ visit(body);
154
+ }
155
+ return found;
156
+ }
157
+ /**
158
+ * The references the element accesses of one project's own files make to the members their
159
+ * index's literal type names, each made by the declaration holding the access or the call.
160
+ */
161
+ export function elementAccessReferences(project, held, chains, root, testFiles) {
162
+ const queries = project.queries;
163
+ const candidates = [];
164
+ const calls = new Map();
165
+ const names = [];
166
+ for (const file of project.ownSourceFiles()) {
167
+ const fileId = held.declarations.get(nodeKey(file, file));
168
+ if (fileId === undefined) {
169
+ continue;
170
+ }
171
+ const visit = (node, from) => {
172
+ const holder = held.declarations.get(nodeKey(file, node)) ?? from;
173
+ if (isElementAccessExpression(node) && !isLiteralIndex(node.argumentExpression)) {
174
+ candidates.push({ file, from: holder, access: node, use: useOf(node) });
175
+ }
176
+ if (isCallExpression(node)) {
177
+ calls.set(node, { file, from: holder });
178
+ }
179
+ if (isIdentifier(node)) {
180
+ names.push(node);
181
+ }
182
+ node.forEachChild((child) => {
183
+ visit(child, holder);
184
+ });
185
+ };
186
+ file.forEachChild((child) => {
187
+ visit(child, fileId);
188
+ });
189
+ }
190
+ if (candidates.length === 0) {
191
+ return [];
192
+ }
193
+ // A parameter index is decided at the calls only where the function is a callable every
194
+ // use of which is a call, so the symbols of the parameters and of every name are read.
195
+ const parameterOf = new Map();
196
+ const byCallable = new Map();
197
+ for (const candidate of candidates) {
198
+ const index = unwrapped(candidate.access.argumentExpression);
199
+ const fn = isIdentifier(index) ? enclosingFunction(candidate.access) : undefined;
200
+ const callable = fn === undefined ? undefined : callableOf(fn);
201
+ if (callable === undefined || !isIdentifier(index)) {
202
+ continue;
203
+ }
204
+ const at = callable.parameters.findIndex((parameter) => isIdentifier(parameter.name) && parameter.name.text === index.text);
205
+ if (at < 0 || assigns(callable.body, index.text)) {
206
+ continue;
207
+ }
208
+ parameterOf.set(candidate, { callable, at });
209
+ byCallable.set(callable.name, [...(byCallable.get(callable.name) ?? []), candidate]);
210
+ }
211
+ const callableNames = [...byCallable.keys()];
212
+ const spelled = new Set(callableNames.map((name) => name.text));
213
+ const sameName = names.filter((name) => spelled.has(name.text));
214
+ const symbols = queries.symbolsAt([...callableNames, ...sameName]);
215
+ const symbolOf = new Map();
216
+ [...callableNames, ...sameName].forEach((node, at) => {
217
+ const symbol = symbols[at];
218
+ if (symbol !== UNANSWERED && symbol !== undefined) {
219
+ symbolOf.set(node, symbol);
220
+ }
221
+ });
222
+ const callsOf = new Map();
223
+ for (const name of callableNames) {
224
+ const symbol = symbolOf.get(name);
225
+ let found = symbol === undefined ? undefined : [];
226
+ for (const use of sameName) {
227
+ if (found === undefined) {
228
+ break;
229
+ }
230
+ if (use === name || symbolOf.get(use) !== symbol) {
231
+ continue;
232
+ }
233
+ const parent = use.parent;
234
+ found =
235
+ isCallExpression(parent) && parent.expression === use ? [...found, parent] : undefined;
236
+ }
237
+ callsOf.set(name, found);
238
+ }
239
+ const lookups = [];
240
+ for (const candidate of candidates) {
241
+ const deferred = parameterOf.get(candidate);
242
+ const sites = deferred === undefined ? undefined : callsOf.get(deferred.callable.name);
243
+ if (deferred === undefined || sites === undefined) {
244
+ lookups.push({
245
+ file: candidate.file,
246
+ from: candidate.from,
247
+ at: candidate.access.argumentExpression,
248
+ object: candidate.access.expression,
249
+ index: candidate.access.argumentExpression,
250
+ use: candidate.use,
251
+ });
252
+ continue;
253
+ }
254
+ // An object that is a parameter too is read as the argument each call passes there.
255
+ const object = unwrapped(candidate.access.expression);
256
+ const objectAt = isIdentifier(object)
257
+ ? deferred.callable.parameters.findIndex((parameter) => isIdentifier(parameter.name) && parameter.name.text === object.text)
258
+ : -1;
259
+ for (const call of sites) {
260
+ const parameter = deferred.callable.parameters[deferred.at];
261
+ const index = call.arguments[deferred.at] ?? parameter?.initializer;
262
+ const site = calls.get(call);
263
+ if (index === undefined || site === undefined) {
264
+ continue;
265
+ }
266
+ lookups.push({
267
+ file: site.file,
268
+ from: site.from,
269
+ at: call,
270
+ object: (objectAt < 0 ? undefined : call.arguments[objectAt]) ?? candidate.access.expression,
271
+ index,
272
+ use: candidate.use,
273
+ });
274
+ }
275
+ }
276
+ const nodes = lookups.flatMap((one) => [one.index, one.object]);
277
+ const types = queries.typesAt(nodes);
278
+ const typeOf = (node, at) => {
279
+ const type = types[at];
280
+ return nodes[at] === node && type !== UNANSWERED ? type : undefined;
281
+ };
282
+ // A type parameter's body serves every instantiation, so it reads what its constraint admits.
283
+ const keysOf = (type) => {
284
+ if (!type.isTypeParameter()) {
285
+ return type;
286
+ }
287
+ const constraint = queries.constraintOf(type);
288
+ return constraint === UNANSWERED || constraint === undefined ? type : constraint;
289
+ };
290
+ const partsOf = (type) => {
291
+ const parts = queries.constituents(type);
292
+ return parts === UNANSWERED ? [type] : parts;
293
+ };
294
+ const found = [];
295
+ lookups.forEach((lookup, at) => {
296
+ const indexType = typeOf(lookup.index, 2 * at);
297
+ const objectType = typeOf(lookup.object, 2 * at + 1);
298
+ const literals = indexType === undefined ? undefined : literalNames(partsOf(keysOf(indexType)));
299
+ if (objectType === undefined || literals === undefined) {
300
+ return;
301
+ }
302
+ const position = renderPosition(lookup.file, root, lookup.at.getStart());
303
+ for (const part of partsOf(objectType)) {
304
+ const properties = queries.propertiesOf(part);
305
+ for (const property of properties === UNANSWERED ? [] : properties) {
306
+ if (!literals.has(property.escapedName)) {
307
+ continue;
308
+ }
309
+ for (const to of chains.declarationsOf(property)) {
310
+ for (const use of lookup.use === "both" ? ["read", "write"] : [lookup.use]) {
311
+ found.push({
312
+ from: lookup.from,
313
+ to,
314
+ position,
315
+ use,
316
+ resolution: "keyed",
317
+ test: testFiles.has(position.path),
318
+ });
319
+ }
320
+ }
321
+ }
322
+ }
323
+ });
324
+ return found;
325
+ }
326
+ /** The names a union of literal types spells, or none where a constituent is no string or number literal. */
327
+ function literalNames(parts) {
328
+ const names = new Set();
329
+ for (const part of parts) {
330
+ if (part.isStringLiteralType()) {
331
+ names.add(part.value);
332
+ }
333
+ else if (part.isNumberLiteralType()) {
334
+ names.add(String(part.value));
335
+ }
336
+ else {
337
+ return undefined;
338
+ }
339
+ }
340
+ return names;
341
+ }